From 49bcbfd7f9149eddf7ccb4384aa874ac2cd343a1 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 31 May 2025 02:11:18 -0700 Subject: [PATCH] New goals page. Upgraded prisma. --- db/postgresql/schema.prisma | 39 -- package.json | 8 +- pnpm-lock.yaml | 517 +++--------------- src/app/(main)/SideNav.tsx | 3 +- src/app/(main)/reports/ReportDeleteButton.tsx | 9 +- .../reports/insights/InsightsParameters.tsx | 2 +- .../websites/[websiteId]/WebsiteNav.tsx | 4 +- .../websites/[websiteId]/goals/Goal.tsx | 171 ++++++ .../[websiteId]/goals/GoalAddButton.tsx | 8 +- .../[websiteId]/goals/GoalAddForm.tsx | 126 +++-- .../websites/[websiteId]/goals/GoalsPage.tsx | 36 +- .../[websiteId]/sessions/SessionsWeekly.tsx | 10 +- src/app/api/reports/goals/route.ts | 58 +- .../api/websites/[websiteId]/goals/route.ts | 94 ++++ src/assets/clock.svg | 1 - src/assets/funnel.svg | 2 +- src/assets/lightbulb.svg | 2 +- src/assets/logo.svg | 2 +- src/assets/magnet.svg | 2 +- src/assets/money.svg | 2 +- src/assets/network.svg | 2 +- src/assets/path.svg | 2 +- src/assets/tag.svg | 2 +- src/assets/target.svg | 2 +- src/assets/user.svg | 1 - src/components/common/EmptyPlaceholder.tsx | 7 +- src/components/common/ErrorMessage.module.css | 15 - src/components/common/ErrorMessage.tsx | 9 +- src/components/common/FilterRecord.tsx | 40 +- src/components/common/LoadingPanel.tsx | 16 +- src/components/common/SectionHeader.tsx | 7 +- src/components/hooks/index.ts | 2 + .../hooks/queries/useDeleteQuery.ts | 11 + src/components/hooks/queries/useGoalQuery.ts | 19 + src/components/hooks/queries/useGoalsQuery.ts | 21 + .../hooks/queries/useReportQuery.ts | 100 +--- .../hooks/queries/useResultQuery.ts | 17 + src/components/icons.ts | 3 + src/components/input/DateFilter.tsx | 6 +- src/components/input/DeleteButton.tsx | 0 src/components/input/TeamsButton.tsx | 3 +- src/components/input/WebsiteDateFilter.tsx | 22 +- src/components/messages.ts | 4 +- src/components/svg/Funnel.tsx | 9 +- src/components/svg/Lightbulb.tsx | 8 +- src/components/svg/Logo.tsx | 14 +- src/components/svg/Magnet.tsx | 1 + src/components/svg/Money.tsx | 8 +- src/components/svg/Network.tsx | 9 +- src/components/svg/Path.tsx | 9 +- src/components/svg/Tag.tsx | 1 + src/components/svg/Target.tsx | 1 + src/components/svg/User.tsx | 14 - src/components/svg/Users.tsx | 13 - src/components/svg/index.ts | 2 - src/lib/auth.ts | 2 +- src/lib/load.ts | 2 +- src/queries/prisma/report.ts | 2 +- src/queries/prisma/team.ts | 2 +- src/queries/prisma/teamUser.ts | 2 +- src/queries/prisma/user.ts | 2 +- src/queries/prisma/website.ts | 2 +- src/queries/sql/reports/getGoal.ts | 77 +++ src/queries/sql/reports/getGoals.ts | 375 ------------- src/queries/sql/sessions/createSession.ts | 2 +- 65 files changed, 769 insertions(+), 1195 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/goals/Goal.tsx create mode 100644 src/app/api/websites/[websiteId]/goals/route.ts delete mode 100644 src/assets/clock.svg delete mode 100644 src/assets/user.svg delete mode 100644 src/components/common/ErrorMessage.module.css create mode 100644 src/components/hooks/queries/useDeleteQuery.ts create mode 100644 src/components/hooks/queries/useGoalQuery.ts create mode 100644 src/components/hooks/queries/useGoalsQuery.ts create mode 100644 src/components/hooks/queries/useResultQuery.ts create mode 100644 src/components/input/DeleteButton.tsx delete mode 100644 src/components/svg/User.tsx delete mode 100644 src/components/svg/Users.tsx create mode 100644 src/queries/sql/reports/getGoal.ts delete mode 100644 src/queries/sql/reports/getGoals.ts diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 5d448dd2..9a78e434 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -235,42 +235,3 @@ model Report { @@index([name]) @@map("report") } - -model Board { - id String @id() @unique() @map("board_id") @db.Uuid - userId String @map("user_id") @db.Uuid - teamId String? @map("team_id") @db.Uuid - name String @map("name") @db.VarChar(200) - description String @map("description") @db.VarChar(500) - parameters Json @map("parameters") @db.JsonB - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - user User @relation(fields: [userId], references: [id]) - team Team? @relation(fields: [teamId], references: [id]) - - @@index([userId]) - @@index([teamId]) - @@index([type]) - @@index([name]) - @@map("board") -} - -model Block { - id String @id() @unique() @map("block_id") @db.Uuid - userId String @map("user_id") @db.Uuid - name String @map("name") @db.VarChar(200) - type String @map("type") @db.VarChar(20) - parameters Json @map("parameters") @db.JsonB - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - user User @relation(fields: [userId], references: [id]) - website Website @relation(fields: [websiteId], references: [id]) - - @@index([userId]) - @@index([websiteId]) - @@index([type]) - @@index([name]) - @@map("block") -} diff --git a/package.json b/package.json index 9113746b..75d879a8 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,12 @@ "@dicebear/core": "^9.2.1", "@fontsource/inter": "^4.5.15", "@hello-pangea/dnd": "^17.0.0", - "@prisma/client": "6.7.0", + "@prisma/client": "^6.8.2", "@prisma/extension-read-replicas": "^0.4.1", "@react-spring/web": "^9.7.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.28.6", - "@umami/react-zen": "^0.119.0", + "@umami/react-zen": "^0.127.0", "@umami/redis-client": "^0.27.0", "bcryptjs": "^2.4.3", "chalk": "^4.1.1", @@ -105,13 +105,13 @@ "isbot": "^5.1.16", "jsonwebtoken": "^9.0.2", "kafkajs": "^2.1.0", - "lucide-react": "^0.509.0", + "lucide-react": "^0.511.0", "maxmind": "^4.3.24", "md5": "^2.3.0", "next": "15.3.1", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "6.7.0", + "prisma": "6.8.2", "pure-rand": "^6.1.0", "react": "^19.0.0", "react-basics": "^0.126.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4dfd3a7..9cbab1fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,11 +27,11 @@ importers: specifier: ^17.0.0 version: 17.0.0(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@prisma/client': - specifier: 6.7.0 - version: 6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3) + specifier: ^6.8.2 + version: 6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3) '@prisma/extension-read-replicas': specifier: ^0.4.1 - version: 0.4.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3)) + version: 0.4.1(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3)) '@react-spring/web': specifier: ^9.7.3 version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -42,8 +42,8 @@ importers: specifier: ^5.28.6 version: 5.77.2(react@19.1.0) '@umami/react-zen': - specifier: ^0.119.0 - version: 0.119.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0)) + specifier: ^0.127.0 + version: 0.127.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0)) '@umami/redis-client': specifier: ^0.27.0 version: 0.27.0 @@ -120,8 +120,8 @@ importers: specifier: ^2.1.0 version: 2.2.4 lucide-react: - specifier: ^0.509.0 - version: 0.509.0(react@19.1.0) + specifier: ^0.511.0 + version: 0.511.0(react@19.1.0) maxmind: specifier: ^4.3.24 version: 4.3.25 @@ -138,8 +138,8 @@ importers: specifier: ^4.1.5 version: 4.1.5 prisma: - specifier: 6.7.0 - version: 6.7.0(typescript@5.8.3) + specifier: 6.8.2 + version: 6.8.2(typescript@5.8.3) pure-rand: specifier: ^6.1.0 version: 6.1.0 @@ -346,45 +346,6 @@ importers: specifier: ^5.5.3 version: 5.8.3 - dist: - dependencies: - '@tanstack/react-query': - specifier: ^4.33.0 - version: 4.36.1(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 - date-fns-tz: - specifier: ^1.1.4 - version: 1.3.8(date-fns@2.30.0) - 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.27.1)(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.27.1)(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@4.9.5) - zustand: - specifier: ^4.3.8 - version: 4.5.7(@types/react@19.1.5)(immer@9.0.21)(react@18.3.1) - packages: '@ampproject/remapping@2.3.0': @@ -1086,9 +1047,6 @@ packages: '@formatjs/ecma402-abstract@2.3.4': resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} - '@formatjs/fast-memoize@1.2.1': - resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==} - '@formatjs/fast-memoize@2.2.3': resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==} @@ -1113,15 +1071,9 @@ packages: '@formatjs/icu-skeleton-parser@1.8.8': resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} - '@formatjs/intl-displaynames@5.4.3': - resolution: {integrity: sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==} - '@formatjs/intl-displaynames@6.8.5': resolution: {integrity: sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==} - '@formatjs/intl-listformat@6.5.3': - resolution: {integrity: sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==} - '@formatjs/intl-listformat@7.7.5': resolution: {integrity: sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==} @@ -1145,14 +1097,6 @@ packages: typescript: optional: true - '@formatjs/intl@2.2.1': - resolution: {integrity: sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==} - peerDependencies: - typescript: ^4.5 - peerDependenciesMeta: - typescript: - optional: true - '@formatjs/ts-transformer@2.13.0': resolution: {integrity: sha512-mu7sHXZk1NWZrQ3eUqugpSYo8x5/tXkrI4uIbFqCEC0eNgQaIcoKgVeDFgDAcgG+cEme2atAUYSFF+DFWC4org==} peerDependencies: @@ -1428,9 +1372,6 @@ packages: resolution: {integrity: sha512-9Hgd/J5nP2U/Vv0teytq9uUAGppiKV9t5tzpsuMLqeqUGD9STxXwKmyZd2v8Z4THSW9rw4+8w7dH7LVlFoym2A==} engines: {node: '>=18.0.0'} - '@next/env@13.5.11': - resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==} - '@next/env@15.3.1': resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} @@ -1440,12 +1381,6 @@ packages: '@next/eslint-plugin-next@14.2.29': resolution: {integrity: sha512-qpxSYiPNJTr9RzqjGi5yom8AIC8Kgdtw4oNIXAB/gDYMDctmfMEv452FRUhT06cWPgcmSsbZiEPYhbFiQtCWTg==} - '@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@15.3.1': resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} engines: {node: '>= 10'} @@ -1458,12 +1393,6 @@ packages: 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@15.3.1': resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} engines: {node: '>= 10'} @@ -1476,12 +1405,6 @@ packages: 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] - '@next/swc-linux-arm64-gnu@15.3.1': resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} engines: {node: '>= 10'} @@ -1494,12 +1417,6 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@13.5.9': - resolution: {integrity: sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-musl@15.3.1': resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} engines: {node: '>= 10'} @@ -1512,12 +1429,6 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@13.5.9': - resolution: {integrity: sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-gnu@15.3.1': resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} engines: {node: '>= 10'} @@ -1530,12 +1441,6 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@13.5.9': - resolution: {integrity: sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-musl@15.3.1': resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} engines: {node: '>= 10'} @@ -1548,12 +1453,6 @@ packages: cpu: [x64] os: [linux] - '@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@15.3.1': resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} engines: {node: '>= 10'} @@ -1566,18 +1465,6 @@ 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@15.3.1': resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} engines: {node: '>= 10'} @@ -1610,8 +1497,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/client@6.7.0': - resolution: {integrity: sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==} + '@prisma/client@6.8.2': + resolution: {integrity: sha512-5II+vbyzv4si6Yunwgkj0qT/iY0zyspttoDrL3R4BYgLdp42/d2C8xdi9vqkrYtKt9H32oFIukvyw3Koz5JoDg==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' @@ -1622,28 +1509,28 @@ packages: typescript: optional: true - '@prisma/config@6.7.0': - resolution: {integrity: sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==} + '@prisma/config@6.8.2': + resolution: {integrity: sha512-ZJY1fF4qRBPdLQ/60wxNtX+eu89c3AkYEcP7L3jkp0IPXCNphCYxikTg55kPJLDOG6P0X+QG5tCv6CmsBRZWFQ==} - '@prisma/debug@6.7.0': - resolution: {integrity: sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==} + '@prisma/debug@6.8.2': + resolution: {integrity: sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==} - '@prisma/engines-version@6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed': - resolution: {integrity: sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==} + '@prisma/engines-version@6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e': + resolution: {integrity: sha512-Rkik9lMyHpFNGaLpPF3H5q5TQTkm/aE7DsGM5m92FZTvWQsvmi6Va8On3pWvqLHOt5aPUvFb/FeZTmphI4CPiQ==} - '@prisma/engines@6.7.0': - resolution: {integrity: sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==} + '@prisma/engines@6.8.2': + resolution: {integrity: sha512-XqAJ//LXjqYRQ1RRabs79KOY4+v6gZOGzbcwDQl0D6n9WBKjV7qdrbd042CwSK0v0lM9MSHsbcFnU2Yn7z8Zlw==} '@prisma/extension-read-replicas@0.4.1': resolution: {integrity: sha512-mCMDloqUKUwx2o5uedTs1FHX3Nxdt1GdRMoeyp1JggjiwOALmIYWhxfIN08M2BZ0w8SKwvJqicJZMjkQYkkijw==} peerDependencies: '@prisma/client': ^6.5.0 - '@prisma/fetch-engine@6.7.0': - resolution: {integrity: sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==} + '@prisma/fetch-engine@6.8.2': + resolution: {integrity: sha512-lCvikWOgaLOfqXGacEKSNeenvj0n3qR5QvZUOmPE2e1Eh8cMYSobxonCg9rqM6FSdTfbpqp9xwhSAOYfNqSW0g==} - '@prisma/get-platform@6.7.0': - resolution: {integrity: sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==} + '@prisma/get-platform@6.8.2': + resolution: {integrity: sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==} '@react-aria/autocomplete@3.0.0-beta.3': resolution: {integrity: sha512-8haBygHNMqVt4Ge90VOk+iVlLW+zhiOGHYz2IKCE6+Sy1dTE6mzhHjxrtwWYnSez/OQLbxjHlwLch4CDd5JkLA==} @@ -2459,27 +2346,9 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@swc/helpers@0.5.2': - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} - - '@tanstack/query-core@4.36.1': - resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} - '@tanstack/query-core@5.77.2': resolution: {integrity: sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q==} - '@tanstack/react-query@4.36.1': - resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - '@tanstack/react-query@5.77.2': resolution: {integrity: sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA==} peerDependencies: @@ -2696,8 +2565,8 @@ packages: resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.119.0': - resolution: {integrity: sha512-OXfRRyaQy3RLAqTQYRs94LKl1i1k8n4QuMxYHZigMgdt25jP/E+kHWwWDONrOK5FK1dZ0PcjiBGilQ9nH+Xp5Q==} + '@umami/react-zen@0.127.0': + resolution: {integrity: sha512-i7HPpkoyE2DJdTVFVbK+aDF5pVYNanNhZBanqptzw69wkt0I9CuG2aVoeLbGH19/j0Pbk7Mfvmw3uDrkGS2Zyw==} '@umami/redis-client@0.27.0': resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} @@ -3134,6 +3003,9 @@ packages: caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + caniuse-lite@1.0.30001720: + resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -3746,11 +3618,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -4200,9 +4067,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} @@ -4439,9 +4303,6 @@ packages: intl-messageformat@10.7.7: resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==} - intl-messageformat@9.13.0: - resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==} - ipaddr.js@2.2.0: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} @@ -4810,6 +4671,10 @@ packages: node-notifier: optional: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -5069,11 +4934,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.509.0: - resolution: {integrity: sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -5229,12 +5089,6 @@ packages: resolution: {integrity: sha512-V6DDh3v8tfZFWbeH6fsL5uBIlWL7SvRgGDaAZWFC5kjQ2xP5dl/mLpWwJQ1Ho6ZbEKVp/351QF1JXYTAmeZ/zA==} 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==} - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -5254,28 +5108,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - 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@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@15.3.1: resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -6000,8 +5832,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@6.7.0: - resolution: {integrity: sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==} + prisma@6.8.2: + resolution: {integrity: sha512-JNricTXQxzDtRS7lCGGOB4g5DJ91eg3nozdubXze3LpcMl1oWwcFddrj++Up3jnRE6X/3gB/xz3V+ecBk/eEGA==} engines: {node: '>=18.18'} hasBin: true peerDependencies: @@ -6078,11 +5910,6 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 - react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -6104,15 +5931,6 @@ packages: peerDependencies: react: '*' - react-intl@5.25.1: - resolution: {integrity: sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==} - peerDependencies: - react: ^16.3.0 || 17 || 18 - typescript: ^4.5 - peerDependenciesMeta: - typescript: - optional: true - react-intl@6.8.9: resolution: {integrity: sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==} peerDependencies: @@ -6168,10 +5986,6 @@ packages: react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^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.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -6358,9 +6172,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -6616,19 +6427,6 @@ packages: style-search@0.1.0: resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} - 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'} @@ -7002,10 +6800,6 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -7805,10 +7599,6 @@ snapshots: decimal.js: 10.5.0 tslib: 2.8.1 - '@formatjs/fast-memoize@1.2.1': - dependencies: - tslib: 2.8.1 - '@formatjs/fast-memoize@2.2.3': dependencies: tslib: 2.8.1 @@ -7850,24 +7640,12 @@ snapshots: '@formatjs/ecma402-abstract': 2.2.4 tslib: 2.8.1 - '@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-displaynames@6.8.5': dependencies: '@formatjs/ecma402-abstract': 2.2.4 '@formatjs/intl-localematcher': 0.5.8 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-listformat@7.7.5': dependencies: '@formatjs/ecma402-abstract': 2.2.4 @@ -7903,18 +7681,6 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@formatjs/intl@2.2.1(typescript@4.9.5)': - 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: 4.9.5 - '@formatjs/ts-transformer@2.13.0(ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(esbuild@0.25.5)(jest@29.7.0(@types/node@22.15.21)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.8.3)))(typescript@5.8.3))': dependencies: intl-messageformat-parser: 6.1.2 @@ -8276,8 +8042,6 @@ snapshots: '@netlify/plugin-nextjs@5.11.2': {} - '@next/env@13.5.11': {} - '@next/env@15.3.1': {} '@next/env@15.3.2': {} @@ -8286,75 +8050,48 @@ snapshots: dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@13.5.9': - optional: true - '@next/swc-darwin-arm64@15.3.1': optional: true '@next/swc-darwin-arm64@15.3.2': optional: true - '@next/swc-darwin-x64@13.5.9': - optional: true - '@next/swc-darwin-x64@15.3.1': optional: true '@next/swc-darwin-x64@15.3.2': optional: true - '@next/swc-linux-arm64-gnu@13.5.9': - optional: true - '@next/swc-linux-arm64-gnu@15.3.1': optional: true '@next/swc-linux-arm64-gnu@15.3.2': optional: true - '@next/swc-linux-arm64-musl@13.5.9': - optional: true - '@next/swc-linux-arm64-musl@15.3.1': optional: true '@next/swc-linux-arm64-musl@15.3.2': optional: true - '@next/swc-linux-x64-gnu@13.5.9': - optional: true - '@next/swc-linux-x64-gnu@15.3.1': optional: true '@next/swc-linux-x64-gnu@15.3.2': optional: true - '@next/swc-linux-x64-musl@13.5.9': - optional: true - '@next/swc-linux-x64-musl@15.3.1': optional: true '@next/swc-linux-x64-musl@15.3.2': optional: true - '@next/swc-win32-arm64-msvc@13.5.9': - optional: true - '@next/swc-win32-arm64-msvc@15.3.1': optional: true '@next/swc-win32-arm64-msvc@15.3.2': 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@15.3.1': optional: true @@ -8378,42 +8115,39 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3)': + '@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3)': optionalDependencies: - prisma: 6.7.0(typescript@5.8.3) + prisma: 6.8.2(typescript@5.8.3) typescript: 5.8.3 - '@prisma/config@6.7.0': + '@prisma/config@6.8.2': dependencies: - esbuild: 0.25.5 - esbuild-register: 3.6.0(esbuild@0.25.5) - transitivePeerDependencies: - - supports-color + jiti: 2.4.2 - '@prisma/debug@6.7.0': {} + '@prisma/debug@6.8.2': {} - '@prisma/engines-version@6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed': {} + '@prisma/engines-version@6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e': {} - '@prisma/engines@6.7.0': + '@prisma/engines@6.8.2': dependencies: - '@prisma/debug': 6.7.0 - '@prisma/engines-version': 6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed - '@prisma/fetch-engine': 6.7.0 - '@prisma/get-platform': 6.7.0 + '@prisma/debug': 6.8.2 + '@prisma/engines-version': 6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e + '@prisma/fetch-engine': 6.8.2 + '@prisma/get-platform': 6.8.2 - '@prisma/extension-read-replicas@0.4.1(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3))': + '@prisma/extension-read-replicas@0.4.1(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))': dependencies: - '@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3) + '@prisma/client': 6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3) - '@prisma/fetch-engine@6.7.0': + '@prisma/fetch-engine@6.8.2': dependencies: - '@prisma/debug': 6.7.0 - '@prisma/engines-version': 6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed - '@prisma/get-platform': 6.7.0 + '@prisma/debug': 6.8.2 + '@prisma/engines-version': 6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e + '@prisma/get-platform': 6.8.2 - '@prisma/get-platform@6.7.0': + '@prisma/get-platform@6.8.2': dependencies: - '@prisma/debug': 6.7.0 + '@prisma/debug': 6.8.2 '@react-aria/autocomplete@3.0.0-beta.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -9694,22 +9428,8 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.2': - dependencies: - tslib: 2.8.1 - - '@tanstack/query-core@4.36.1': {} - '@tanstack/query-core@5.77.2': {} - '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@tanstack/query-core': 4.36.1 - react: 18.3.1 - use-sync-external-store: 1.5.0(react@18.3.1) - optionalDependencies: - react-dom: 18.3.1(react@18.3.1) - '@tanstack/react-query@5.77.2(react@19.1.0)': dependencies: '@tanstack/query-core': 5.77.2 @@ -9976,7 +9696,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 - '@umami/react-zen@0.119.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))': + '@umami/react-zen@0.127.0(@babel/core@7.27.1)(@types/react@19.1.5)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))': dependencies: '@fontsource/jetbrains-mono': 5.2.5 '@internationalized/date': 3.8.1 @@ -10487,6 +10207,8 @@ snapshots: caniuse-lite@1.0.30001718: {} + caniuse-lite@1.0.30001720: {} + caseless@0.12.0: {} chalk@2.4.2: @@ -11203,13 +10925,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-register@3.6.0(esbuild@0.25.5): - dependencies: - debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.5 - transitivePeerDependencies: - - supports-color - esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -11798,8 +11513,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - glob@10.3.10: dependencies: foreground-child: 3.3.1 @@ -12029,13 +11742,6 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.9.4 tslib: 2.8.1 - 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.2.0: {} is-array-buffer@3.0.5: @@ -12585,6 +12291,8 @@ snapshots: - supports-color - ts-node + jiti@2.4.2: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -12855,10 +12563,6 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.509.0(react@19.1.0): - dependencies: - react: 19.1.0 - lucide-react@0.511.0(react@19.1.0): dependencies: react: 19.1.0 @@ -13010,12 +12714,6 @@ snapshots: mmdb-lib@2.2.0: {} - moment-timezone@0.5.48: - dependencies: - moment: 2.30.1 - - moment@2.30.1: {} - ms@2.1.2: {} ms@2.1.3: {} @@ -13026,40 +12724,6 @@ snapshots: natural-compare@1.4.0: {} - next-basics@0.36.0(next@13.5.11(@babel/core@7.27.1)(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.2 - next: 13.5.11(@babel/core@7.27.1)(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@13.5.11(@babel/core@7.27.1)(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.30001718 - 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.27.1)(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@15.3.1(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.3.1 @@ -13091,7 +12755,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001718 + caniuse-lite: 1.0.30001720 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -13783,15 +13447,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@6.7.0(typescript@5.8.3): + prisma@6.8.2(typescript@5.8.3): dependencies: - '@prisma/config': 6.7.0 - '@prisma/engines': 6.7.0 + '@prisma/config': 6.8.2 + '@prisma/engines': 6.8.2 optionalDependencies: - fsevents: 2.3.3 typescript: 5.8.3 - transitivePeerDependencies: - - supports-color process@0.11.10: {} @@ -13925,12 +13586,6 @@ snapshots: react-hook-form: 7.56.4(react@19.1.0) react-window: 1.8.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - 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.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -13949,22 +13604,6 @@ snapshots: dependencies: react: 19.1.0 - react-intl@5.25.1(react@18.3.1)(typescript@4.9.5): - dependencies: - '@formatjs/ecma402-abstract': 1.11.4 - '@formatjs/icu-messageformat-parser': 2.1.0 - '@formatjs/intl': 2.2.1(typescript@4.9.5) - '@formatjs/intl-displaynames': 5.4.3 - '@formatjs/intl-listformat': 6.5.3 - '@types/hoist-non-react-statics': 3.3.6 - '@types/react': 18.3.22 - hoist-non-react-statics: 3.3.2 - intl-messageformat: 9.13.0 - react: 18.3.1 - tslib: 2.8.1 - optionalDependencies: - typescript: 4.9.5 - react-intl@6.8.9(react@19.1.0)(typescript@5.8.3): dependencies: '@formatjs/ecma402-abstract': 2.2.4 @@ -14047,10 +13686,6 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - react@19.1.0: {} read-babelrc-up@1.1.0: @@ -14284,10 +13919,6 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - scheduler@0.26.0: {} schema-utils@2.7.1: @@ -14604,13 +14235,6 @@ snapshots: style-search@0.1.0: {} - styled-jsx@5.1.1(@babel/core@7.27.1)(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - optionalDependencies: - '@babel/core': 7.27.1 - styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): dependencies: client-only: 0.0.1 @@ -14992,10 +14616,6 @@ snapshots: dependencies: react: 19.1.0 - use-sync-external-store@1.5.0(react@18.3.1): - dependencies: - react: 18.3.1 - use-sync-external-store@1.5.0(react@19.1.0): dependencies: react: 19.1.0 @@ -15041,11 +14661,6 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.4.0: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - web-streams-polyfill@3.3.3: {} which-boxed-primitive@1.1.1: @@ -15185,14 +14800,6 @@ snapshots: zod@3.25.30: {} - zustand@4.5.7(@types/react@19.1.5)(immer@9.0.21)(react@18.3.1): - dependencies: - use-sync-external-store: 1.5.0(react@18.3.1) - optionalDependencies: - '@types/react': 19.1.5 - immer: 9.0.21 - react: 18.3.1 - zustand@4.5.7(@types/react@19.1.5)(immer@9.0.21)(react@19.1.0): dependencies: use-sync-external-store: 1.5.0(react@19.1.0) diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index ceb02beb..c6f99d96 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -9,8 +9,7 @@ import { Grid2X2, Settings, } from '@/components/icons'; -import { useMessages, useNavigation } from '@/components/hooks'; -import useGlobalState from '@/components/hooks/useGlobalState'; +import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; export function SideNav(props: any) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index e57ea0bb..d64e2b56 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -1,7 +1,8 @@ import { Button, Icon, Modal, DialogTrigger, Dialog, Text } from '@umami/react-zen'; -import { useApi, useMessages, useModified } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; import { Trash } from '@/components/icons'; import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery'; export function ReportDeleteButton({ reportId, @@ -13,11 +14,7 @@ export function ReportDeleteButton({ onDelete?: () => void; }) { const { formatMessage, labels, messages } = useMessages(); - const { del, useMutation } = useApi(); - const { mutate, isPending, error } = useMutation({ - mutationFn: reportId => del(`/reports/${reportId}`), - }); - const { touch } = useModified(); + const { mutate, isPending, error, touch } = useDeleteQuery(`/reports/${reportId}`); const handleConfirm = (close: () => void) => { mutate(reportId as any, { diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx index 1844deb4..944cfe58 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.tsx +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -18,7 +18,7 @@ export function InsightsParameters() { }; return ( -
+ {parametersSelected && } {parametersSelected && } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 5fe4fbb4..f76fa920 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -67,7 +67,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { id: 'journeys', label: formatMessage(labels.journey), icon: , - path: '/goals', + path: '/journeys', }, { id: 'retention', @@ -106,7 +106,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { - {icon} + {icon} {label} diff --git a/src/app/(main)/websites/[websiteId]/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/goals/Goal.tsx new file mode 100644 index 00000000..3fbc5947 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/goals/Goal.tsx @@ -0,0 +1,171 @@ +import { useState } from 'react'; +import { + Grid, + Row, + Column, + Text, + Icon, + Button, + MenuTrigger, + Menu, + MenuItem, + Popover, + ProgressBar, + Dialog, + Modal, + AlertDialog, +} from '@umami/react-zen'; +import { useMessages, useResultQuery } from '@/components/hooks'; +import { Edit, More, Trash, File, Lightning, User, Eye } from '@/components/icons'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { formatLongNumber } from '@/lib/format'; +import { GoalAddForm } from '@/app/(main)/websites/[websiteId]/goals/GoalAddForm'; +import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery'; + +export interface GoalProps { + id: string; + name: string; + type: string; + parameters: { + name: string; + type: string; + value: string; + }; + websiteId: string; + startDate: Date; + endDate: Date; +} + +export type GoalData = { num: number; total: number }; + +export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading } = useResultQuery(type, { + ...parameters, + websiteId, + dateRange: { + startDate, + endDate, + }, + }); + const isPage = parameters?.type === 'page'; + + return ( + + + + + + + {name} + + + + + + + + + + {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)} + + {formatMessage(labels.conversionRate)} + + + + {parameters.type === 'page' ? : } + {parameters.value} + + + {isPage ? : } + {`${formatLongNumber( + data?.num, + )} / ${formatLongNumber(data?.total)}`} + + + + + + {data?.total ? Math.round((+data?.num / +data?.total) * 100) : '0'}% + + + + + ); +} + +const ActionsButton = ({ id, websiteId }: { id: string; websiteId: string }) => { + const { formatMessage, labels } = useMessages(); + const [showEdit, setShowEdit] = useState(false); + const [showDelete, setShowDelete] = useState(false); + const { mutate, touch } = useDeleteQuery(`/reports/${id}`); + + const handleAction = (id: any) => { + if (id === 'edit') { + setShowEdit(true); + } else if (id === 'delete') { + setShowDelete(true); + } + }; + + const handleClose = () => { + setShowEdit(false); + setShowDelete(false); + }; + + const handleDelete = async () => { + mutate(null, { + onSuccess: async () => { + touch(`goals`); + setShowDelete(false); + }, + }); + }; + + return ( + <> + + + + + + + + + {formatMessage(labels.edit)} + + + + + + {formatMessage(labels.delete)} + + + + + + {showEdit && ( + + + + )} + {showDelete && ( + + )} + + + ); +}; diff --git a/src/app/(main)/websites/[websiteId]/goals/GoalAddButton.tsx b/src/app/(main)/websites/[websiteId]/goals/GoalAddButton.tsx index 82592b78..f092cc0d 100644 --- a/src/app/(main)/websites/[websiteId]/goals/GoalAddButton.tsx +++ b/src/app/(main)/websites/[websiteId]/goals/GoalAddButton.tsx @@ -1,4 +1,4 @@ -import { Button, MenuTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen'; +import { Button, DialogTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { GoalAddForm } from './GoalAddForm'; import { Plus } from '@/components/icons'; @@ -7,7 +7,7 @@ export function GoalAddButton({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); return ( - + - + {({ close }) => } - + ); } diff --git a/src/app/(main)/websites/[websiteId]/goals/GoalAddForm.tsx b/src/app/(main)/websites/[websiteId]/goals/GoalAddForm.tsx index c7119516..8bff4b56 100644 --- a/src/app/(main)/websites/[websiteId]/goals/GoalAddForm.tsx +++ b/src/app/(main)/websites/[websiteId]/goals/GoalAddForm.tsx @@ -2,72 +2,116 @@ import { Form, FormField, TextField, - Select, + Grid, FormButtons, FormSubmitButton, Button, + RadioGroup, + Radio, + Text, + Icon, + Loading, } from '@umami/react-zen'; -import { useApi, useMessages } from '@/components/hooks'; +import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks'; +import { File, Lightning } from '@/components/icons'; + +const defaultValues = { + name: '', + type: 'page', + value: '', +}; export function GoalAddForm({ + id, websiteId, onSave, onClose, }: { + id?: string; websiteId: string; onSave?: () => void; onClose?: () => void; }) { const { formatMessage, labels } = useMessages(); + const { touch } = useModified(); const { post, useMutation } = useApi(); + const { data } = useReportQuery(id); const { mutate, error, isPending } = useMutation({ - mutationFn: (data: any) => post('/websites', { ...data }), + mutationFn: (params: any) => post(`/websites/${websiteId}/goals`, params), }); const handleSubmit = async (data: any) => { - mutate(data, { - onSuccess: async () => { - onSave?.(); - onClose?.(); + mutate( + { id, ...data }, + { + onSuccess: async () => { + onSave?.(); + onClose?.(); + touch('goals'); + }, }, - }); + ); }; - const items = [ - { id: 'page', label: formatMessage(labels.page) }, - { id: 'event', label: formatMessage(labels.event) }, - ]; + if (id && !data) { + return ; + } return ( - - {websiteId} - - - - - type === 'string')} value={operator} @@ -50,15 +40,15 @@ export function FilterRecord({ }} onChange?.(name, e.target.value)} /> - - - - - - + + + + + + ); } diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index fd42b884..dfb2dabf 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -1,36 +1,36 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; -import { Loading } from '@umami/react-zen'; +import { Spinner, Dots } from '@umami/react-zen'; import { ErrorMessage } from '@/components/common/ErrorMessage'; import { Empty } from '@/components/common/Empty'; import styles from './LoadingPanel.module.css'; export function LoadingPanel({ - data, error, + isEmpty, isFetched, isLoading, loadingIcon = 'dots', + renderEmpty = () => , className, children, }: { data?: any; error?: Error; + isEmpty?: boolean; isFetched?: boolean; isLoading?: boolean; loadingIcon?: 'dots' | 'spinner'; - isEmpty?: boolean; + renderEmpty?: () => ReactNode; className?: string; children: ReactNode; }) { - const isEmpty = !isLoading && isFetched && data && Array.isArray(data) && data.length === 0; - return (
- {isLoading && !isFetched && } + {isLoading && !isFetched && (loadingIcon === 'dots' ? : )} {error && } - {!error && isEmpty && } - {!error && !isEmpty && data && children} + {!error && !isLoading && isEmpty && renderEmpty()} + {!error && !isLoading && !isEmpty && children}
); } diff --git a/src/components/common/SectionHeader.tsx b/src/components/common/SectionHeader.tsx index 1b29f6a8..752e0a5b 100644 --- a/src/components/common/SectionHeader.tsx +++ b/src/components/common/SectionHeader.tsx @@ -1,11 +1,12 @@ import { ReactNode } from 'react'; -import { Heading, Icon, Row, Text } from '@umami/react-zen'; +import { Heading, Icon, Row, Text, RowProps } from '@umami/react-zen'; export function SectionHeader({ title, description, icon, children, + ...props }: { title?: string; description?: string; @@ -13,9 +14,9 @@ export function SectionHeader({ allowEdit?: boolean; className?: string; children?: ReactNode; -}) { +} & RowProps) { return ( - + {icon && {icon}} {title && {title}} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 89fbdc4b..a516d7b2 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -3,8 +3,10 @@ export * from './queries/useActiveUsersQuery'; export * from './queries/useEventDataEventsQuery'; export * from './queries/useEventDataPropertiesQuery'; export * from './queries/useEventDataValuesQuery'; +export * from './queries/useGoalsQuery'; export * from './queries/useLoginQuery'; export * from './queries/useRealtimeQuery'; +export * from './queries/useResultQuery'; export * from './queries/useReportQuery'; export * from './queries/useReportsQuery'; export * from './queries/useRetentionQuery'; diff --git a/src/components/hooks/queries/useDeleteQuery.ts b/src/components/hooks/queries/useDeleteQuery.ts new file mode 100644 index 00000000..bf5d8f5d --- /dev/null +++ b/src/components/hooks/queries/useDeleteQuery.ts @@ -0,0 +1,11 @@ +import { useApi, useModified } from '@/components/hooks'; + +export function useDeleteQuery(path: string, params?: { [key: string]: any }) { + const { del, useMutation } = useApi(); + const { mutate, isPending, error } = useMutation({ + mutationFn: () => del(path, params), + }); + const { touch } = useModified(); + + return { mutate, isPending, error, touch }; +} diff --git a/src/components/hooks/queries/useGoalQuery.ts b/src/components/hooks/queries/useGoalQuery.ts new file mode 100644 index 00000000..6018074b --- /dev/null +++ b/src/components/hooks/queries/useGoalQuery.ts @@ -0,0 +1,19 @@ +import { useApi } from '../useApi'; +import { usePagedQuery } from '../usePagedQuery'; + +export function useGoalQuery( + { websiteId, reportId }: { websiteId: string; reportId: string }, + params?: { [key: string]: string | number }, +) { + const { post } = useApi(); + + return usePagedQuery({ + queryKey: ['goal', { websiteId, reportId, ...params }], + queryFn: (data: any) => { + return post(`/reports/goals`, { + ...data, + ...params, + }); + }, + }); +} diff --git a/src/components/hooks/queries/useGoalsQuery.ts b/src/components/hooks/queries/useGoalsQuery.ts new file mode 100644 index 00000000..cf6d0511 --- /dev/null +++ b/src/components/hooks/queries/useGoalsQuery.ts @@ -0,0 +1,21 @@ +import { useApi } from '../useApi'; +import { usePagedQuery } from '../usePagedQuery'; +import { useModified } from '../useModified'; + +export function useGoalsQuery( + { websiteId }: { websiteId: string }, + params?: { [key: string]: string | number }, +) { + const { get } = useApi(); + const { modified } = useModified(`goals`); + + return usePagedQuery({ + queryKey: ['goals', { websiteId, modified, ...params }], + queryFn: (data: any) => { + return get(`/websites/${websiteId}/goals`, { + ...data, + ...params, + }); + }, + }); +} diff --git a/src/components/hooks/queries/useReportQuery.ts b/src/components/hooks/queries/useReportQuery.ts index c5b139a5..b4cad2a0 100644 --- a/src/components/hooks/queries/useReportQuery.ts +++ b/src/components/hooks/queries/useReportQuery.ts @@ -1,94 +1,16 @@ -import { produce } from 'immer'; -import { useCallback, useEffect, useState } from 'react'; import { useApi } from '../useApi'; -import { useTimezone } from '../useTimezone'; -import { useMessages } from '../useMessages'; -import { parseDateRange } from '@/lib/date'; +import { useModified } from '../useModified'; -export function useReportQuery( - reportId: string, - defaultParameters?: { type: string; parameters: { [key: string]: any } }, -) { - const [report, setReport] = useState(null); - const [isRunning, setIsRunning] = useState(false); - const { get, post } = useApi(); - const { timezone } = useTimezone(); - const { formatMessage, labels } = useMessages(); +export function useReportQuery(reportId: string) { + const { get, useQuery } = useApi(); + const { modified } = useModified(`report:${reportId}`); - const baseParameters = { - name: formatMessage(labels.untitled), - description: '', - parameters: {}, - }; - - const loadReport = async (id: string) => { - const data: any = await get(`/reports/${id}`); - - const { dateRange } = data?.parameters || {}; - - data.parameters = { - ...defaultParameters?.parameters, - ...data.parameters, - dateRange: parseDateRange(dateRange.value), - }; - - setReport(data); - }; - - const runReport = useCallback( - async (parameters: { [key: string]: any }) => { - setIsRunning(true); - - const { type } = report; - - const data = await post(`/reports/${type}`, { ...parameters, timezone }); - - setReport( - produce((state: any) => { - state.parameters = { ...defaultParameters?.parameters, ...parameters }; - state.data = data; - - return state; - }), - ); - - setIsRunning(false); + return useQuery({ + queryKey: ['report', { reportId, modified }], + queryFn: (data: any) => { + return get(`/reports/${reportId}`, { + ...data, + }); }, - [report, timezone], - ); - - const updateReport = useCallback( - async (data: { [x: string]: any; parameters: any }) => { - setReport( - produce((state: any) => { - const { parameters, ...rest } = data; - - if (parameters) { - state.parameters = { - ...defaultParameters?.parameters, - ...state.parameters, - ...parameters, - }; - } - - for (const key in rest) { - state[key] = rest[key]; - } - - return state; - }), - ); - }, - [report], - ); - - useEffect(() => { - if (!reportId) { - setReport({ ...baseParameters, ...defaultParameters }); - } else { - loadReport(reportId); - } - }, [reportId]); - - return { report, runReport, updateReport, isRunning }; + }); } diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts new file mode 100644 index 00000000..a67ff7a8 --- /dev/null +++ b/src/components/hooks/queries/useResultQuery.ts @@ -0,0 +1,17 @@ +import { useApi } from '@/components/hooks'; +import { UseQueryOptions, QueryKey } from '@tanstack/react-query'; + +export function useResultQuery( + type: string, + params?: { [key: string]: any }, + options?: Omit, 'queryKey' | 'queryFn'>, +) { + const { post, useQuery } = useApi(); + + return useQuery({ + queryKey: ['reports', type, params], + queryFn: () => post(`/reports/${type}`, params), + enabled: !!type, + ...options, + }); +} diff --git a/src/components/icons.ts b/src/components/icons.ts index ca8da939..ad341def 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -3,12 +3,14 @@ export { ArrowRight as Arrow, Calendar, ChevronRight as Chevron, + Clock, X as Close, Copy, Edit, Ellipsis, Eye, ExternalLink, + File, Globe, Grid2X2, LayoutDashboard, @@ -28,6 +30,7 @@ export { SquarePen, Sun, Trash, + User, Users, } from 'lucide-react'; export * from '@/components/svg'; diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index 02ff39d1..8f120860 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -1,5 +1,5 @@ import { useState, Key, Fragment } from 'react'; -import { Modal, Select, ListItem, ListSeparator, Dialog } from '@umami/react-zen'; +import { Modal, Select, ListItem, ListSeparator, Dialog, Row } from '@umami/react-zen'; import { endOfYear } from 'date-fns'; import { DatePickerForm } from '@/components/metrics/DatePickerForm'; import { useMessages } from '@/components/hooks'; @@ -100,7 +100,7 @@ export function DateFilter({ }; return ( - <> + - {formatMessage(labels.previousPeriod)} - {formatMessage(labels.previousYear)} - + + + )} {!isAllTime && allowCompare && ( diff --git a/src/components/messages.ts b/src/components/messages.ts index 9e56f013..81cbc047 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -107,6 +107,7 @@ export const labels = defineMessages({ sum: { id: 'label.sum', defaultMessage: 'Sum' }, event: { id: 'label.event', defaultMessage: 'Event' }, events: { id: 'label.events', defaultMessage: 'Events' }, + eventName: { id: 'label.event-name', defaultMessage: 'Event name' }, query: { id: 'label.query', defaultMessage: 'Query' }, queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' }, back: { id: 'label.back', defaultMessage: 'Back' }, @@ -268,7 +269,8 @@ export const labels = defineMessages({ id: 'label.utm-description', defaultMessage: 'Track your campaigns through UTM parameters.', }, - conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion Step' }, + conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion step' }, + conversionRate: { id: 'label.conversion-ratep', defaultMessage: 'Conversion rate' }, steps: { id: 'label.steps', defaultMessage: 'Steps' }, startStep: { id: 'label.start-step', defaultMessage: 'Start Step' }, endStep: { id: 'label.end-step', defaultMessage: 'End Step' }, diff --git a/src/components/svg/Funnel.tsx b/src/components/svg/Funnel.tsx index abcbc7fd..05a24378 100644 --- a/src/components/svg/Funnel.tsx +++ b/src/components/svg/Funnel.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; import type { SVGProps } from 'react'; const SvgFunnel = (props: SVGProps) => ( - + diff --git a/src/components/svg/Lightbulb.tsx b/src/components/svg/Lightbulb.tsx index 684b58ef..9a16b5c4 100644 --- a/src/components/svg/Lightbulb.tsx +++ b/src/components/svg/Lightbulb.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; import type { SVGProps } from 'react'; const SvgLightbulb = (props: SVGProps) => ( - + diff --git a/src/components/svg/Logo.tsx b/src/components/svg/Logo.tsx index 34dafe6f..79fa813c 100644 --- a/src/components/svg/Logo.tsx +++ b/src/components/svg/Logo.tsx @@ -5,19 +5,13 @@ const SvgLogo = (props: SVGProps) => ( xmlns="http://www.w3.org/2000/svg" width={20} height={20} + fill="currentColor" + stroke="currentColor" viewBox="0 0 428 389.11" {...props} > - - + + ); export default SvgLogo; diff --git a/src/components/svg/Magnet.tsx b/src/components/svg/Magnet.tsx index 9fe13bdd..3cd4ded7 100644 --- a/src/components/svg/Magnet.tsx +++ b/src/components/svg/Magnet.tsx @@ -5,6 +5,7 @@ const SvgMagnet = (props: SVGProps) => ( xmlns="http://www.w3.org/2000/svg" width={512} height={512} + fill="currentColor" viewBox="0 0 508.467 508.467" {...props} > diff --git a/src/components/svg/Money.tsx b/src/components/svg/Money.tsx index d5095b6d..a3e74364 100644 --- a/src/components/svg/Money.tsx +++ b/src/components/svg/Money.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; import type { SVGProps } from 'react'; const SvgMoney = (props: SVGProps) => ( - + diff --git a/src/components/svg/Network.tsx b/src/components/svg/Network.tsx index d8aa9a6e..32860aa7 100644 --- a/src/components/svg/Network.tsx +++ b/src/components/svg/Network.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; import type { SVGProps } from 'react'; const SvgNetwork = (props: SVGProps) => ( - + ); diff --git a/src/components/svg/Path.tsx b/src/components/svg/Path.tsx index 6627964f..c49b1b3f 100644 --- a/src/components/svg/Path.tsx +++ b/src/components/svg/Path.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; import type { SVGProps } from 'react'; const SvgPath = (props: SVGProps) => ( - + ); diff --git a/src/components/svg/Tag.tsx b/src/components/svg/Tag.tsx index f43d80aa..e908664d 100644 --- a/src/components/svg/Tag.tsx +++ b/src/components/svg/Tag.tsx @@ -5,6 +5,7 @@ const SvgTag = (props: SVGProps) => ( xmlns="http://www.w3.org/2000/svg" width="437pt" height="437pt" + fill="currentColor" viewBox="0 0 437.004 437" {...props} > diff --git a/src/components/svg/Target.tsx b/src/components/svg/Target.tsx index bc0de9b7..5102e2de 100644 --- a/src/components/svg/Target.tsx +++ b/src/components/svg/Target.tsx @@ -5,6 +5,7 @@ const SvgTarget = (props: SVGProps) => ( xmlns="http://www.w3.org/2000/svg" width={512} height={512} + fill="currentColor" fillRule="evenodd" strokeLinejoin="round" strokeMiterlimit={2} diff --git a/src/components/svg/User.tsx b/src/components/svg/User.tsx deleted file mode 100644 index 980c521c..00000000 --- a/src/components/svg/User.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import type { SVGProps } from 'react'; -const SvgUser = (props: SVGProps) => ( - - - -); -export default SvgUser; diff --git a/src/components/svg/Users.tsx b/src/components/svg/Users.tsx deleted file mode 100644 index b14a7947..00000000 --- a/src/components/svg/Users.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; -import type { SVGProps } from 'react'; -const SvgUsers = (props: SVGProps) => ( - - - -); -export default SvgUsers; diff --git a/src/components/svg/index.ts b/src/components/svg/index.ts index 004d8638..86c3ea94 100644 --- a/src/components/svg/index.ts +++ b/src/components/svg/index.ts @@ -4,7 +4,6 @@ export { default as Bars } from './Bars'; export { default as Bolt } from './Bolt'; export { default as Bookmark } from './Bookmark'; export { default as Change } from './Change'; -export { default as Clock } from './Clock'; export { default as Compare } from './Compare'; export { default as Dashboard } from './Dashboard'; export { default as Expand } from './Expand'; @@ -31,6 +30,5 @@ export { default as Security } from './Security'; export { default as Speaker } from './Speaker'; export { default as Tag } from './Tag'; export { default as Target } from './Target'; -export { default as User } from './User'; export { default as Visitor } from './Visitor'; export { default as Website } from './Website'; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d67566b8..b5bc683f 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs'; -import { Report } from '@prisma/client'; +import { Report } from '@/generated/prisma'; import redis from '@/lib/redis'; import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; diff --git a/src/lib/load.ts b/src/lib/load.ts index d9aa23c2..631ba775 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,4 +1,4 @@ -import { Website, Session } from '@prisma/client'; +import { Website, Session } from '@/generated/prisma'; import redis from '@/lib/redis'; import { getWebsiteSession, getWebsite } from '@/queries'; diff --git a/src/queries/prisma/report.ts b/src/queries/prisma/report.ts index 4feb9fb8..07b9d571 100644 --- a/src/queries/prisma/report.ts +++ b/src/queries/prisma/report.ts @@ -1,4 +1,4 @@ -import { Prisma, Report } from '@prisma/client'; +import { Prisma, Report } from '@/generated/prisma'; import prisma from '@/lib/prisma'; import { PageResult, PageParams } from '@/lib/types'; import ReportFindManyArgs = Prisma.ReportFindManyArgs; diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index 9862fff2..e69f91db 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -1,4 +1,4 @@ -import { Prisma, Team } from '@prisma/client'; +import { Prisma, Team } from '@/generated/prisma'; import { ROLES } from '@/lib/constants'; import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; diff --git a/src/queries/prisma/teamUser.ts b/src/queries/prisma/teamUser.ts index 1478ae5b..6fdba0f3 100644 --- a/src/queries/prisma/teamUser.ts +++ b/src/queries/prisma/teamUser.ts @@ -1,4 +1,4 @@ -import { Prisma, TeamUser } from '@prisma/client'; +import { Prisma, TeamUser } from '@/generated/prisma'; import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; import { PageResult, PageParams } from '@/lib/types'; diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 2e6a478f..9dffd84d 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { Prisma } from '@/generated/prisma'; import { ROLES } from '@/lib/constants'; import prisma from '@/lib/prisma'; import { PageResult, Role, User, PageParams } from '@/lib/types'; diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index 25328914..5d248a40 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -1,4 +1,4 @@ -import { Prisma, Website } from '@prisma/client'; +import { Prisma, Website } from '@/generated/prisma'; import redis from '@/lib/redis'; import prisma from '@/lib/prisma'; import { PageResult, PageParams } from '@/lib/types'; diff --git a/src/queries/sql/reports/getGoal.ts b/src/queries/sql/reports/getGoal.ts new file mode 100644 index 00000000..2a0b49e5 --- /dev/null +++ b/src/queries/sql/reports/getGoal.ts @@ -0,0 +1,77 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; + +export interface GoalCriteria { + startDate: Date; + endDate: Date; + type: string; + value: string; + operator?: string; + property?: string; +} + +export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, criteria: GoalCriteria) { + const { type, value } = criteria; + const { rawQuery, parseFilters } = prisma; + const { filterQuery, dateQuery, params } = await parseFilters(websiteId, criteria); + const isPage = type === 'page'; + const column = isPage ? 'url_path' : 'event_name'; + const eventType = isPage ? 1 : 2; + + return rawQuery( + ` + select count(*) as num, + ( + select count(${isPage ? '*' : 'distinct session_id'}) + from website_event + where website_id = {websiteId:UUID} + and event_type = ${eventType} + ${dateQuery} + ) as total + from website_event + where website_id = {websiteId:UUID} + and event_type = ${eventType} + and ${column} = {value:String} + ${dateQuery} + ${filterQuery} + `, + { ...params, value }, + ); +} + +async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) { + const { type, value } = criteria; + const { rawQuery, parseFilters } = clickhouse; + const { filterQuery, dateQuery, params } = await parseFilters(websiteId, criteria); + const isPage = type === 'page'; + const column = isPage ? 'url_path' : 'event_name'; + const eventType = isPage ? 1 : 2; + + return rawQuery( + ` + select count(*) as num, + ( + select count(${isPage ? '*' : 'distinct session_id'}) + from website_event + where website_id = {websiteId:UUID} + and event_type = ${eventType} + ${dateQuery} + ) as total + from website_event + where website_id = {websiteId:UUID} + and event_type = ${eventType} + and ${column} = {value:String} + ${dateQuery} + ${filterQuery} + `, + { ...params, value }, + ).then(results => results?.[0]); +} diff --git a/src/queries/sql/reports/getGoals.ts b/src/queries/sql/reports/getGoals.ts deleted file mode 100644 index eda76050..00000000 --- a/src/queries/sql/reports/getGoals.ts +++ /dev/null @@ -1,375 +0,0 @@ -import clickhouse from '@/lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; -import prisma from '@/lib/prisma'; - -export async function getGoals( - ...args: [ - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - goals: { type: string; value: string; goal: number; operator?: string }[]; - }, - ] -) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - goals: { type: string; value: string; goal: number; operator?: string }[]; - }, -): Promise { - const { startDate, endDate, goals } = criteria; - const { rawQuery } = prisma; - - const urls = goals.filter(a => a.type === 'url'); - const events = goals.filter(a => a.type === 'event'); - const eventData = goals.filter(a => a.type === 'event-data'); - - const hasUrl = urls.length > 0; - const hasEvent = events.length > 0; - const hasEventData = eventData.length > 0; - - function getParameters( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlParam = urls.reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); - - const eventParam = events.reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); - - const eventDataParam = eventData.reduce((acc, cv, i) => { - acc[`eventData${i}`] = cv.value; - acc[`property${i}`] = cv.property; - return acc; - }, {}); - - return { - urls: { ...urlParam, startDate, endDate, websiteId }, - events: { ...eventParam, startDate, endDate, websiteId }, - eventData: { ...eventDataParam, startDate, endDate, websiteId }, - }; - } - - function getColumns( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlColumns = urls - .map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i},`) - .join('\n') - .slice(0, -1); - const eventColumns = events - .map((a, i) => `COUNT(CASE WHEN event_name = {{event${i}}} THEN 1 END) AS EVENT${i},`) - .join('\n') - .slice(0, -1); - const eventDataColumns = eventData - .map( - (a, i) => - `${ - a.operator === 'average' ? 'avg' : a.operator - }(CASE WHEN event_name = {{eventData${i}}} AND data_key = {{property${i}}} THEN ${ - a.operator === 'count' ? '1' : 'number_value' - } END) AS EVENT_DATA${i},`, - ) - .join('\n') - .slice(0, -1); - - return { urls: urlColumns, events: eventColumns, eventData: eventDataColumns }; - } - - function getWhere( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(','); - const eventWhere = events.map((a, i) => `{{event${i}}}`).join(','); - const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i}}}`).join(','); - const eventDataKeyWhere = eventData.map((a, i) => `{{property${i}}}`).join(','); - - return { - urls: `and url_path in (${urlWhere})`, - events: `and event_name in (${eventWhere})`, - eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, - }; - } - - const parameters = getParameters(urls, events, eventData); - const columns = getColumns(urls, events, eventData); - const where = getWhere(urls, events, eventData); - - const urlResults = hasUrl - ? await rawQuery( - ` - select - ${columns.urls} - from website_event - where website_id = {{websiteId::uuid}} - ${where.urls} - and created_at between {{startDate}} and {{endDate}} - `, - parameters.urls, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => ({ - ...urls[i], - goal: Number(urls[i].goal), - result: Number(results[key]), - })); - }) - : []; - - const eventResults = hasEvent - ? await rawQuery( - ` - select - ${columns.events} - from website_event - where website_id = {{websiteId::uuid}} - ${where.events} - and created_at between {{startDate}} and {{endDate}} - `, - parameters.events, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => { - return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) }; - }); - }) - : []; - - const eventDataResults = hasEventData - ? await rawQuery( - ` - select - ${columns.eventData} - from website_event w - join event_data d - on d.website_event_id = w.event_id - where w.website_id = {{websiteId::uuid}} - ${where.eventData} - and w.created_at between {{startDate}} and {{endDate}} - `, - parameters.eventData, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => { - return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; - }); - }) - : []; - - return [...urlResults, ...eventResults, ...eventDataResults]; -} - -async function clickhouseQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - goals: { type: string; value: string; goal: number; operator?: string; property?: string }[]; - }, -): Promise<{ type: string; value: string; goal: number; result: number }[]> { - const { startDate, endDate, goals } = criteria; - const { rawQuery } = clickhouse; - - const urls = goals.filter(a => a.type === 'url'); - const events = goals.filter(a => a.type === 'event'); - const eventData = goals.filter(a => a.type === 'event-data'); - - const hasUrl = urls.length > 0; - const hasEvent = events.length > 0; - const hasEventData = eventData.length > 0; - - function getParameters( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlParam = urls.reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); - - const eventParam = events.reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); - - const eventDataParam = eventData.reduce((acc, cv, i) => { - acc[`eventData${i}`] = cv.value; - acc[`property${i}`] = cv.property; - return acc; - }, {}); - - return { - urls: { ...urlParam, startDate, endDate, websiteId }, - events: { ...eventParam, startDate, endDate, websiteId }, - eventData: { ...eventDataParam, startDate, endDate, websiteId }, - }; - } - - function getColumns( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlColumns = urls - .map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`) - .join('\n') - .slice(0, -1); - const eventColumns = events - .map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i},`) - .join('\n') - .slice(0, -1); - const eventDataColumns = eventData - .map( - (a, i) => - `${a.operator === 'average' ? 'avg' : a.operator}If(${ - a.operator !== 'count' ? 'number_value, ' : '' - }event_name = {eventData${i}:String} AND data_key = {property${i}:String}) AS EVENT_DATA${i},`, - ) - .join('\n') - .slice(0, -1); - - return { url: urlColumns, events: eventColumns, eventData: eventDataColumns }; - } - - function getWhere( - urls: { type: string; value: string; goal: number }[], - events: { type: string; value: string; goal: number }[], - eventData: { - type: string; - value: string; - goal: number; - operator?: string; - property?: string; - }[], - ) { - const urlWhere = urls.map((a, i) => `{url${i}:String}`).join(','); - const eventWhere = events.map((a, i) => `{event${i}:String}`).join(','); - const eventDataNameWhere = eventData.map((a, i) => `{eventData${i}:String}`).join(','); - const eventDataKeyWhere = eventData.map((a, i) => `{property${i}:String}`).join(','); - - return { - urls: `and url_path in (${urlWhere})`, - events: `and event_name in (${eventWhere})`, - eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, - }; - } - - const parameters = getParameters(urls, events, eventData); - const columns = getColumns(urls, events, eventData); - const where = getWhere(urls, events, eventData); - - const urlResults = hasUrl - ? await rawQuery( - ` - select - ${columns.url} - from website_event - where website_id = {websiteId:UUID} - ${where.urls} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - `, - parameters.urls, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => { - return { ...urls[i], goal: Number(urls[i].goal), result: Number(results[key]) }; - }); - }) - : []; - - const eventResults = hasEvent - ? await rawQuery( - ` - select - ${columns.events} - from website_event - where website_id = {websiteId:UUID} - ${where.events} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - `, - parameters.events, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => { - return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) }; - }); - }) - : []; - - const eventDataResults = hasEventData - ? await rawQuery( - ` - select - ${columns.eventData} - from event_data - where website_id = {websiteId:UUID} - ${where.eventData} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - `, - parameters.eventData, - ).then(a => { - const results = a[0]; - - return Object.keys(results).map((key, i) => { - return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; - }); - }) - : []; - - return [...urlResults, ...eventResults, ...eventDataResults]; -} diff --git a/src/queries/sql/sessions/createSession.ts b/src/queries/sql/sessions/createSession.ts index 9d1402b1..726fcfac 100644 --- a/src/queries/sql/sessions/createSession.ts +++ b/src/queries/sql/sessions/createSession.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { Prisma } from '@/generated/prisma'; import prisma from '@/lib/prisma'; export async function createSession(data: Prisma.SessionCreateInput) {