diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml
index 2404918b..d48567e6 100644
--- a/.github/ISSUE_TEMPLATE/1.bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml
@@ -24,13 +24,13 @@ body:
render: shell
- type: input
attributes:
- label: Which Umami version are you using? (if relevant)
+ label: Which Umami version are you using?
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
- type: input
attributes:
- label: Which browser are you using? (if relevant)
- description: 'For example: Chrome, Edge, Firefox, etc'
+ label: How are you deploying your application?
+ description: 'For example: Vercel, Railway, Docker, etc'
- type: input
attributes:
- label: How are you deploying your application? (if relevant)
- description: 'For example: Vercel, Railway, Docker, etc'
+ label: Which browser are you using?
+ description: 'For example: Chrome, Edge, Firefox, etc'
diff --git a/.gitignore b/.gitignore
index 753389d1..8b543f6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ package-lock.json
/dist
/generated
/src/generated
+pm2.yml
# misc
.DS_Store
@@ -30,6 +31,8 @@ package-lock.json
*.log
.vscode
.tool-versions
+.claude
+nul
# debug
npm-debug.log*
diff --git a/next.config.ts b/next.config.ts
index 99dcca0d..1a4e2e0e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -8,6 +8,7 @@ const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
const corsMaxAge = process.env.CORS_MAX_AGE || '';
+const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
@@ -170,6 +171,7 @@ export default {
cloudMode,
cloudUrl,
currentVersion: pkg.version,
+ defaultCurrency,
defaultLocale,
},
basePath,
diff --git a/package.json b/package.json
index 865c08c0..d162be63 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
},
"type": "module",
"scripts": {
- "dev": "next dev -p 3001 --turbo",
+ "dev": "next dev -p 3002 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
@@ -97,12 +97,12 @@
"is-docker": "^3.0.0",
"is-localhost-ip": "^2.0.0",
"isbot": "^5.1.31",
- "jsonwebtoken": "^9.0.2",
+ "jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
"kafkajs": "^2.1.0",
"lucide-react": "^0.543.0",
"maxmind": "^5.0.0",
- "next": "^15.5.9",
+ "next": "^15.5.10",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"papaparse": "^5.5.3",
@@ -143,8 +143,8 @@
"@types/react-window": "^1.8.8",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "^10.1.0",
- "cypress": "^13.6.6",
- "extract-react-intl-messages": "^4.1.1",
+ "cypress": "^15.8.0",
+ "extract-react-intl-messages": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.2.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd7f90b7..406ccae0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,8 +117,8 @@ importers:
specifier: ^5.1.31
version: 5.1.32
jsonwebtoken:
- specifier: ^9.0.2
- version: 9.0.2
+ specifier: ^9.0.3
+ version: 9.0.3
jszip:
specifier: ^3.10.1
version: 3.10.1
@@ -132,8 +132,8 @@ importers:
specifier: ^5.0.0
version: 5.0.1
next:
- specifier: ^15.5.9
- version: 15.5.9(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ specifier: ^15.5.10
+ version: 15.5.10(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
node-fetch:
specifier: ^3.2.8
version: 3.3.2
@@ -250,11 +250,11 @@ importers:
specifier: ^10.1.0
version: 10.1.0
cypress:
- specifier: ^13.6.6
- version: 13.17.0
+ specifier: ^15.8.0
+ version: 15.9.0
extract-react-intl-messages:
- specifier: ^4.1.1
- version: 4.1.1(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
+ specifier: ^5.0.0
+ version: 5.0.0(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -566,10 +566,6 @@ packages:
resolution: {integrity: sha512-co2spjR7wZoZ3Ck0H/jv76bpiuO3oJHtOmq9/gxFiod2DcT9NFg01u/hXcG8MJFnEJuMB6e3vGqS6IOnLwHqRw==}
engines: {node: '>=16'}
- '@colors/colors@1.5.0':
- resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
- engines: {node: '>=0.1.90'}
-
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -687,8 +683,8 @@ packages:
peerDependencies:
postcss-selector-parser: ^6.0.13
- '@cypress/request@3.0.9':
- resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==}
+ '@cypress/request@3.0.10':
+ resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
engines: {node: '>= 6'}
'@cypress/xvfb@1.2.4':
@@ -887,11 +883,8 @@ packages:
peerDependencies:
'@dicebear/core': ^9.0.0
- '@emnapi/runtime@1.5.0':
- resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
-
- '@emnapi/runtime@1.7.1':
- resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
+ '@emnapi/runtime@1.8.1':
+ resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
@@ -1288,75 +1281,38 @@ packages:
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
- '@img/sharp-darwin-arm64@0.34.3':
- resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [darwin]
-
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.34.3':
- resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [darwin]
-
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-darwin-arm64@1.2.0':
- resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
- cpu: [arm64]
- os: [darwin]
-
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.2.0':
- resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
- cpu: [x64]
- os: [darwin]
-
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.2.0':
- resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
- cpu: [arm64]
- os: [linux]
-
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.2.0':
- resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
- cpu: [arm]
- os: [linux]
-
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-ppc64@1.2.0':
- resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
- cpu: [ppc64]
- os: [linux]
-
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
@@ -1367,76 +1323,38 @@ packages:
cpu: [riscv64]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.2.0':
- resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
- cpu: [s390x]
- os: [linux]
-
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.2.0':
- resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
- cpu: [x64]
- os: [linux]
-
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
- resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
- cpu: [arm64]
- os: [linux]
-
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.2.0':
- resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
- cpu: [x64]
- os: [linux]
-
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.34.3':
- resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
-
'@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]
- '@img/sharp-linux-arm@0.34.3':
- resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm]
- os: [linux]
-
'@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]
- '@img/sharp-linux-ppc64@0.34.3':
- resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [ppc64]
- os: [linux]
-
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -1449,94 +1367,47 @@ packages:
cpu: [riscv64]
os: [linux]
- '@img/sharp-linux-s390x@0.34.3':
- resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [s390x]
- os: [linux]
-
'@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]
- '@img/sharp-linux-x64@0.34.3':
- resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
-
'@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]
- '@img/sharp-linuxmusl-arm64@0.34.3':
- resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
-
'@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]
- '@img/sharp-linuxmusl-x64@0.34.3':
- resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
-
'@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]
- '@img/sharp-wasm32@0.34.3':
- resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [wasm32]
-
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.34.3':
- resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [win32]
-
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.34.3':
- resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [ia32]
- os: [win32]
-
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.34.3':
- resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [win32]
-
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -1695,11 +1566,11 @@ packages:
resolution: {integrity: sha512-HXm94tteOuA0FYwhkxjYIPe0zta+Dsu0wz7LnhfqVlaYcRaOLjHtd2vgfmpz3np/fx9TQg3gCfqGkXt2a9i7Aw==}
engines: {node: '>=18.0.0'}
- '@next/env@15.5.9':
- resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==}
+ '@next/env@15.5.10':
+ resolution: {integrity: sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A==}
- '@next/env@16.0.10':
- resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
+ '@next/env@16.1.5':
+ resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
'@next/swc-darwin-arm64@15.5.7':
resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
@@ -1707,8 +1578,8 @@ packages:
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-arm64@16.0.10':
- resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==}
+ '@next/swc-darwin-arm64@16.1.5':
+ resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -1719,8 +1590,8 @@ packages:
cpu: [x64]
os: [darwin]
- '@next/swc-darwin-x64@16.0.10':
- resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==}
+ '@next/swc-darwin-x64@16.1.5':
+ resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -1731,8 +1602,8 @@ packages:
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-gnu@16.0.10':
- resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==}
+ '@next/swc-linux-arm64-gnu@16.1.5':
+ resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1743,8 +1614,8 @@ packages:
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@16.0.10':
- resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
+ '@next/swc-linux-arm64-musl@16.1.5':
+ resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1755,8 +1626,8 @@ packages:
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-gnu@16.0.10':
- resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
+ '@next/swc-linux-x64-gnu@16.1.5':
+ resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1767,8 +1638,8 @@ packages:
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@16.0.10':
- resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
+ '@next/swc-linux-x64-musl@16.1.5':
+ resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1779,8 +1650,8 @@ packages:
cpu: [arm64]
os: [win32]
- '@next/swc-win32-arm64-msvc@16.0.10':
- resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
+ '@next/swc-win32-arm64-msvc@16.1.5':
+ resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -1791,8 +1662,8 @@ packages:
cpu: [x64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.0.10':
- resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==}
+ '@next/swc-win32-x64-msvc@16.1.5':
+ resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2816,8 +2687,8 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
- '@swc/helpers@0.5.17':
- resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
+ '@swc/helpers@0.5.18':
+ resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
'@tanstack/query-core@5.90.11':
resolution: {integrity: sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==}
@@ -2940,6 +2811,9 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+ '@types/tmp@0.2.6':
+ resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
+
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
@@ -3101,9 +2975,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
- async@3.2.6:
- resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
-
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -3169,6 +3040,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.9.18:
+ resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==}
+ hasBin: true
+
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
@@ -3276,14 +3151,8 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
- caniuse-lite@1.0.30001735:
- resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
-
- caniuse-lite@1.0.30001741:
- resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==}
-
- caniuse-lite@1.0.30001759:
- resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
+ caniuse-lite@1.0.30001766:
+ resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@@ -3314,10 +3183,6 @@ packages:
chart.js: '>=2.8.0'
date-fns: '>=2.0.0'
- check-more-types@2.24.0:
- resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==}
- engines: {node: '>= 0.8.0'}
-
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -3355,8 +3220,8 @@ packages:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
- cli-table3@0.6.5:
- resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
+ cli-table3@0.6.1:
+ resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==}
engines: {node: 10.* || >= 12.*}
cli-truncate@2.1.0:
@@ -3402,13 +3267,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- color-string@1.9.1:
- resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
-
- color@4.2.3:
- resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
- engines: {node: '>=12.5.0'}
-
colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@@ -3418,6 +3276,10 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+ colors@1.4.0:
+ resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
+ engines: {node: '>=0.1.90'}
+
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -3616,9 +3478,9 @@ packages:
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
engines: {node: '>=0.10.0'}
- cypress@13.17.0:
- resolution: {integrity: sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==}
- engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
+ cypress@15.9.0:
+ resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==}
+ engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
hasBin: true
d3-array@2.12.1:
@@ -3776,10 +3638,6 @@ packages:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
- detect-libc@2.0.4:
- resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
- engines: {node: '>=8'}
-
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -3987,9 +3845,9 @@ packages:
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
- extract-react-intl-messages@4.1.1:
- resolution: {integrity: sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg==}
- engines: {node: '>=10'}
+ extract-react-intl-messages@5.0.0:
+ resolution: {integrity: sha512-7K1aA3WxhhjBXsuZ2buZm5MLuPHjzkbErV2qqhf0m0K9RMqdwe6mYrOAMZ+1z1bfrngwQ2Iv44+RLjILO8qPdA==}
+ engines: {node: '>=20'}
hasBin: true
extract-zip@2.0.1:
@@ -4048,6 +3906,10 @@ packages:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
+ file-entry-cache@5.0.1:
+ resolution: {integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==}
+ engines: {node: '>=4'}
+
file-entry-cache@7.0.2:
resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==}
engines: {node: '>=12.0.0'}
@@ -4067,6 +3929,10 @@ packages:
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
+ flat-cache@2.0.1:
+ resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==}
+ engines: {node: '>=4'}
+
flat-cache@3.2.0:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -4075,6 +3941,9 @@ packages:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
+ flatted@2.0.2:
+ resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==}
+
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@@ -4180,9 +4049,6 @@ packages:
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
- getos@3.2.1:
- resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
-
getpass@0.1.7:
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
@@ -4285,6 +4151,10 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
+ hasha@5.2.2:
+ resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
+ engines: {node: '>=8'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -4429,9 +4299,6 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
- is-arrayish@0.3.2:
- resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
-
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -4871,8 +4738,8 @@ packages:
jsonify@0.0.1:
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
- jsonwebtoken@9.0.2:
- resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
+ jsonwebtoken@9.0.3:
+ resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jsprim@2.0.2:
@@ -4882,11 +4749,11 @@ packages:
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
- jwa@1.4.2:
- resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
+ jwa@2.0.1:
+ resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
- jws@3.2.2:
- resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+ jws@4.0.1:
+ resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
kafkajs@2.2.4:
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
@@ -4909,10 +4776,6 @@ packages:
known-css-properties@0.36.0:
resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==}
- lazy-ass@1.6.0:
- resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
- engines: {node: '> 0.8'}
-
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -5006,10 +4869,6 @@ packages:
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
- lodash.pick@4.4.0:
- resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==}
- deprecated: This package is deprecated. Use destructuring assignment syntax instead.
-
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
@@ -5236,8 +5095,8 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
- next@15.5.9:
- resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
+ next@15.5.10:
+ resolution: {integrity: sha512-r0X65PNwyDDyOrWNKpQoZvOatw7BcsTPRKdwEqtc9cj3wv7mbBIk9tKed4klRaFXJdX0rugpuMTHslDrAU1bBg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -5257,8 +5116,8 @@ packages:
sass:
optional: true
- next@16.0.10:
- resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==}
+ next@16.1.5:
+ resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -6085,8 +5944,8 @@ packages:
pure-rand@7.0.1:
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
- qs@6.14.0:
- resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
+ qs@6.14.1:
+ resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3:
@@ -6310,6 +6169,11 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+ rimraf@2.6.3:
+ resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
@@ -6429,10 +6293,6 @@ packages:
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
- sharp@0.34.3:
- resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
-
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -6480,9 +6340,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
- simple-swizzle@0.2.2:
- resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
-
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -6743,6 +6600,12 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ systeminformation@5.30.5:
+ resolution: {integrity: sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A==}
+ engines: {node: '>=8.0.0'}
+ os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
+ hasBin: true
+
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
@@ -7134,6 +6997,10 @@ packages:
resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==}
engines: {node: '>=8.3'}
+ write@1.0.3:
+ resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==}
+ engines: {node: '>=4'}
+
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -7448,9 +7315,6 @@ snapshots:
dependencies:
'@clickhouse/client-common': 1.14.0
- '@colors/colors@1.5.0':
- optional: true
-
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -7548,7 +7412,7 @@ snapshots:
dependencies:
postcss-selector-parser: 6.1.2
- '@cypress/request@3.0.9':
+ '@cypress/request@3.0.10':
dependencies:
aws-sign2: 0.7.0
aws4: 1.13.2
@@ -7563,7 +7427,7 @@ snapshots:
json-stringify-safe: 5.0.1
mime-types: 2.1.35
performance-now: 2.1.0
- qs: 6.14.0
+ qs: 6.14.1
safe-buffer: 5.2.1
tough-cookie: 5.1.2
tunnel-agent: 0.6.0
@@ -7736,12 +7600,7 @@ snapshots:
dependencies:
'@dicebear/core': 9.2.4
- '@emnapi/runtime@1.5.0':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.7.1':
+ '@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -8033,108 +7892,56 @@ snapshots:
'@img/colour@1.0.0':
optional: true
- '@img/sharp-darwin-arm64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.2.0
- optional: true
-
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
- '@img/sharp-darwin-x64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.2.0
- optional: true
-
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
- '@img/sharp-libvips-darwin-arm64@1.2.0':
- optional: true
-
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.2.0':
- optional: true
-
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.2.0':
- optional: true
-
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.2.0':
- optional: true
-
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.2.0':
- optional: true
-
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.2.0':
- optional: true
-
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.2.0':
- optional: true
-
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
- optional: true
-
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.2.0':
- optional: true
-
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
- '@img/sharp-linux-arm64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.2.0
- optional: true
-
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
- '@img/sharp-linux-arm@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.2.0
- optional: true
-
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
- '@img/sharp-linux-ppc64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.2.0
- optional: true
-
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
@@ -8145,90 +7952,56 @@ snapshots:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
- '@img/sharp-linux-s390x@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.2.0
- optional: true
-
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
- '@img/sharp-linux-x64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.2.0
- optional: true
-
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
- optional: true
-
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
- '@img/sharp-linuxmusl-x64@0.34.3':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.2.0
- optional: true
-
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
- '@img/sharp-wasm32@0.34.3':
- dependencies:
- '@emnapi/runtime': 1.5.0
- optional: true
-
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.7.1
- optional: true
-
- '@img/sharp-win32-arm64@0.34.3':
+ '@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
- '@img/sharp-win32-ia32@0.34.3':
- optional: true
-
'@img/sharp-win32-ia32@0.34.5':
optional: true
- '@img/sharp-win32-x64@0.34.3':
- optional: true
-
'@img/sharp-win32-x64@0.34.5':
optional: true
'@internationalized/date@3.10.0':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
'@internationalized/message@3.1.8':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
intl-messageformat: 10.7.18
'@internationalized/number@3.6.5':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
'@internationalized/string@3.2.7':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
'@isaacs/balanced-match@4.0.1': {}
@@ -8476,56 +8249,56 @@ snapshots:
'@netlify/plugin-nextjs@5.15.1': {}
- '@next/env@15.5.9': {}
+ '@next/env@15.5.10': {}
- '@next/env@16.0.10': {}
+ '@next/env@16.1.5': {}
'@next/swc-darwin-arm64@15.5.7':
optional: true
- '@next/swc-darwin-arm64@16.0.10':
+ '@next/swc-darwin-arm64@16.1.5':
optional: true
'@next/swc-darwin-x64@15.5.7':
optional: true
- '@next/swc-darwin-x64@16.0.10':
+ '@next/swc-darwin-x64@16.1.5':
optional: true
'@next/swc-linux-arm64-gnu@15.5.7':
optional: true
- '@next/swc-linux-arm64-gnu@16.0.10':
+ '@next/swc-linux-arm64-gnu@16.1.5':
optional: true
'@next/swc-linux-arm64-musl@15.5.7':
optional: true
- '@next/swc-linux-arm64-musl@16.0.10':
+ '@next/swc-linux-arm64-musl@16.1.5':
optional: true
'@next/swc-linux-x64-gnu@15.5.7':
optional: true
- '@next/swc-linux-x64-gnu@16.0.10':
+ '@next/swc-linux-x64-gnu@16.1.5':
optional: true
'@next/swc-linux-x64-musl@15.5.7':
optional: true
- '@next/swc-linux-x64-musl@16.0.10':
+ '@next/swc-linux-x64-musl@16.1.5':
optional: true
'@next/swc-win32-arm64-msvc@15.5.7':
optional: true
- '@next/swc-win32-arm64-msvc@16.0.10':
+ '@next/swc-win32-arm64-msvc@16.1.5':
optional: true
'@next/swc-win32-x64-msvc@15.5.7':
optional: true
- '@next/swc-win32-x64-msvc@16.0.10':
+ '@next/swc-win32-x64-msvc@16.1.5':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -8609,7 +8382,7 @@ snapshots:
'@react-types/autocomplete': 3.0.0-alpha.35(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8620,7 +8393,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/breadcrumbs': 3.7.17(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8632,7 +8405,7 @@ snapshots:
'@react-stately/toggle': 3.9.2(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8647,7 +8420,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/calendar': 3.8.0(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8663,7 +8436,7 @@ snapshots:
'@react-stately/toggle': 3.9.2(react@19.2.3)
'@react-types/checkbox': 3.10.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8673,7 +8446,7 @@ snapshots:
'@react-aria/ssr': 3.9.10(react@19.2.3)
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
@@ -8692,7 +8465,7 @@ snapshots:
'@react-stately/form': 3.2.2(react@19.2.3)
'@react-types/color': 3.1.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8713,7 +8486,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/combobox': 3.13.9(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8736,7 +8509,7 @@ snapshots:
'@react-types/datepicker': 3.13.2(react@19.2.3)
'@react-types/dialog': 3.5.22(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8747,7 +8520,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/dialog': 3.5.22(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8757,7 +8530,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-stately/disclosure': 3.0.8(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8773,7 +8546,7 @@ snapshots:
'@react-stately/dnd': 3.7.1(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8782,7 +8555,7 @@ snapshots:
'@react-aria/interactions': 3.25.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
clsx: 2.1.1
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8793,7 +8566,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-stately/form': 3.2.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8811,7 +8584,7 @@ snapshots:
'@react-types/checkbox': 3.10.2(react@19.2.3)
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8826,7 +8599,7 @@ snapshots:
'@react-stately/list': 3.13.1(react@19.2.3)
'@react-stately/tree': 3.9.3(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8839,7 +8612,7 @@ snapshots:
'@react-aria/ssr': 3.9.10(react@19.2.3)
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8849,7 +8622,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-stately/flags': 3.1.2
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8857,7 +8630,7 @@ snapshots:
dependencies:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8865,7 +8638,7 @@ snapshots:
dependencies:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
@@ -8876,7 +8649,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/link': 3.6.5(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8890,13 +8663,13 @@ snapshots:
'@react-stately/list': 3.13.1(react@19.2.3)
'@react-types/listbox': 3.7.4(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@react-aria/live-announcer@3.4.4':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
'@react-aria/menu@3.19.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
@@ -8913,7 +8686,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/menu': 3.10.5(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8922,7 +8695,7 @@ snapshots:
'@react-aria/progress': 3.4.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/meter': 3.4.13(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8938,7 +8711,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/numberfield': 3.8.15(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8954,7 +8727,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/overlays': 3.9.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8965,7 +8738,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/progress': 3.5.16(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8980,7 +8753,7 @@ snapshots:
'@react-stately/radio': 3.11.2(react@19.2.3)
'@react-types/radio': 3.9.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -8993,7 +8766,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/searchfield': 3.6.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9012,7 +8785,7 @@ snapshots:
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/select': 3.11.0(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9024,7 +8797,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-stately/selection': 3.20.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9032,7 +8805,7 @@ snapshots:
dependencies:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9045,7 +8818,7 @@ snapshots:
'@react-stately/slider': 3.7.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/slider': 3.8.2(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9056,13 +8829,13 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@react-aria/ssr@3.9.10(react@19.2.3)':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-aria/switch@3.7.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
@@ -9071,7 +8844,7 @@ snapshots:
'@react-stately/toggle': 3.9.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/switch': 3.5.15(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9091,7 +8864,7 @@ snapshots:
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/table': 3.13.4(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9104,7 +8877,7 @@ snapshots:
'@react-stately/tabs': 3.8.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/tabs': 3.3.19(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9119,7 +8892,7 @@ snapshots:
'@react-stately/list': 3.13.1(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9133,7 +8906,7 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/textfield': 3.12.6(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9146,7 +8919,7 @@ snapshots:
'@react-stately/toast': 3.1.2(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9157,7 +8930,7 @@ snapshots:
'@react-stately/toggle': 3.9.2(react@19.2.3)
'@react-types/checkbox': 3.10.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9167,7 +8940,7 @@ snapshots:
'@react-aria/i18n': 3.12.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9178,7 +8951,7 @@ snapshots:
'@react-stately/tooltip': 3.5.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/tooltip': 3.4.21(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9191,7 +8964,7 @@ snapshots:
'@react-stately/tree': 3.9.3(react@19.2.3)
'@react-types/button': 3.14.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9201,7 +8974,7 @@ snapshots:
'@react-stately/flags': 3.1.2
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
clsx: 2.1.1
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9213,7 +8986,7 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-stately/virtualizer': 4.4.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9222,7 +8995,7 @@ snapshots:
'@react-aria/interactions': 3.25.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-aria/utils': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9293,7 +9066,7 @@ snapshots:
'@react-stately/autocomplete@3.0.0-beta.3(react@19.2.3)':
dependencies:
'@react-stately/utils': 3.10.8(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/calendar@3.9.0(react@19.2.3)':
@@ -9302,7 +9075,7 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/calendar': 3.8.0(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/checkbox@3.7.2(react@19.2.3)':
@@ -9311,13 +9084,13 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/checkbox': 3.10.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/collections@3.12.8(react@19.2.3)':
dependencies:
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/color@3.9.2(react@19.2.3)':
@@ -9330,7 +9103,7 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/color': 3.1.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/combobox@3.12.0(react@19.2.3)':
@@ -9342,13 +9115,13 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/combobox': 3.13.9(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/data@3.14.1(react@19.2.3)':
dependencies:
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/datepicker@3.15.2(react@19.2.3)':
@@ -9360,31 +9133,31 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/datepicker': 3.13.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/disclosure@3.0.8(react@19.2.3)':
dependencies:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/dnd@3.7.1(react@19.2.3)':
dependencies:
'@react-stately/selection': 3.20.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/flags@3.1.2':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
'@react-stately/form@3.2.2(react@19.2.3)':
dependencies:
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/grid@3.11.6(react@19.2.3)':
@@ -9393,7 +9166,7 @@ snapshots:
'@react-stately/selection': 3.20.6(react@19.2.3)
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/layout@4.5.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
@@ -9404,7 +9177,7 @@ snapshots:
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/table': 3.13.4(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9414,7 +9187,7 @@ snapshots:
'@react-stately/selection': 3.20.6(react@19.2.3)
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/menu@3.9.8(react@19.2.3)':
@@ -9422,7 +9195,7 @@ snapshots:
'@react-stately/overlays': 3.6.20(react@19.2.3)
'@react-types/menu': 3.10.5(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/numberfield@3.10.2(react@19.2.3)':
@@ -9431,14 +9204,14 @@ snapshots:
'@react-stately/form': 3.2.2(react@19.2.3)
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/numberfield': 3.8.15(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/overlays@3.6.20(react@19.2.3)':
dependencies:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/overlays': 3.9.2(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/radio@3.11.2(react@19.2.3)':
@@ -9447,14 +9220,14 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/radio': 3.9.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/searchfield@3.5.16(react@19.2.3)':
dependencies:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/searchfield': 3.6.6(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/select@3.8.0(react@19.2.3)':
@@ -9465,7 +9238,7 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/select': 3.11.0(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/selection@3.20.6(react@19.2.3)':
@@ -9473,7 +9246,7 @@ snapshots:
'@react-stately/collections': 3.12.8(react@19.2.3)
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/slider@3.7.2(react@19.2.3)':
@@ -9481,7 +9254,7 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/slider': 3.8.2(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/table@3.15.1(react@19.2.3)':
@@ -9494,7 +9267,7 @@ snapshots:
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/table': 3.13.4(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/tabs@3.8.6(react@19.2.3)':
@@ -9502,12 +9275,12 @@ snapshots:
'@react-stately/list': 3.13.1(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/tabs': 3.3.19(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/toast@3.1.2(react@19.2.3)':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
use-sync-external-store: 1.6.0(react@19.2.3)
@@ -9516,14 +9289,14 @@ snapshots:
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/checkbox': 3.10.2(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/tooltip@3.5.8(react@19.2.3)':
dependencies:
'@react-stately/overlays': 3.6.20(react@19.2.3)
'@react-types/tooltip': 3.4.21(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/tree@3.9.3(react@19.2.3)':
@@ -9532,18 +9305,18 @@ snapshots:
'@react-stately/selection': 3.20.6(react@19.2.3)
'@react-stately/utils': 3.10.8(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/utils@3.10.8(react@19.2.3)':
dependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
'@react-stately/virtualizer@4.4.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@react-types/shared': 3.32.1(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -9975,7 +9748,7 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@swc/helpers@0.5.17':
+ '@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
@@ -10102,6 +9875,8 @@ snapshots:
'@types/stack-utils@2.0.3': {}
+ '@types/tmp@0.2.6': {}
+
'@types/use-sync-external-store@0.0.6': {}
'@types/yargs-parser@21.0.3': {}
@@ -10125,7 +9900,7 @@ snapshots:
glob: 13.0.0
highlight.js: 11.11.1
lucide-react: 0.555.0(react@19.2.3)
- next: 16.0.10(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ next: 16.1.5(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-aria-components: 1.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-dom: 19.2.3(react@19.2.3)
@@ -10308,8 +10083,6 @@ snapshots:
async-function@1.0.0: {}
- async@3.2.6: {}
-
asynckit@0.4.0: {}
at-least-node@1.0.0: {}
@@ -10317,7 +10090,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.25.2
- caniuse-lite: 1.0.30001735
+ caniuse-lite: 1.0.30001766
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@@ -10413,6 +10186,8 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.9.18: {}
+
bcrypt-pbkdf@1.0.2:
dependencies:
tweetnacl: 0.14.5
@@ -10440,7 +10215,7 @@ snapshots:
browserslist@4.25.2:
dependencies:
- caniuse-lite: 1.0.30001735
+ caniuse-lite: 1.0.30001766
electron-to-chromium: 1.5.202
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.2)
@@ -10527,15 +10302,11 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
browserslist: 4.25.2
- caniuse-lite: 1.0.30001741
+ caniuse-lite: 1.0.30001766
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
- caniuse-lite@1.0.30001735: {}
-
- caniuse-lite@1.0.30001741: {}
-
- caniuse-lite@1.0.30001759: {}
+ caniuse-lite@1.0.30001766: {}
caseless@0.12.0: {}
@@ -10563,8 +10334,6 @@ snapshots:
chart.js: 4.5.1
date-fns: 2.30.0
- check-more-types@2.24.0: {}
-
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -10593,11 +10362,11 @@ snapshots:
dependencies:
restore-cursor: 5.1.0
- cli-table3@0.6.5:
+ cli-table3@0.6.1:
dependencies:
string-width: 4.2.3
optionalDependencies:
- '@colors/colors': 1.5.0
+ colors: 1.4.0
cli-truncate@2.1.0:
dependencies:
@@ -10637,24 +10406,15 @@ snapshots:
color-name@1.1.4: {}
- color-string@1.9.1:
- dependencies:
- color-name: 1.1.4
- simple-swizzle: 0.2.2
- optional: true
-
- color@4.2.3:
- dependencies:
- color-convert: 2.0.1
- color-string: 1.9.1
- optional: true
-
colord@2.9.3: {}
colorette@1.4.0: {}
colorette@2.0.20: {}
+ colors@1.4.0:
+ optional: true
+
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -10870,22 +10630,22 @@ snapshots:
dependencies:
array-find-index: 1.0.2
- cypress@13.17.0:
+ cypress@15.9.0:
dependencies:
- '@cypress/request': 3.0.9
+ '@cypress/request': 3.0.10
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.9
+ '@types/tmp': 0.2.6
arch: 2.2.0
blob-util: 2.0.2
bluebird: 3.7.2
buffer: 5.7.1
cachedir: 2.4.0
chalk: 4.1.2
- check-more-types: 2.24.0
ci-info: 4.3.0
cli-cursor: 3.1.0
- cli-table3: 0.6.5
+ cli-table3: 0.6.1
commander: 6.2.1
common-tags: 1.8.2
dayjs: 1.11.13
@@ -10897,9 +10657,8 @@ snapshots:
extract-zip: 2.0.1(supports-color@8.1.1)
figures: 3.2.0
fs-extra: 9.1.0
- getos: 3.2.1
+ hasha: 5.2.2
is-installed-globally: 0.4.0
- lazy-ass: 1.6.0
listr2: 3.14.0(enquirer@2.4.1)
lodash: 4.17.21
log-symbols: 4.1.0
@@ -10909,8 +10668,8 @@ snapshots:
process: 0.11.10
proxy-from-env: 1.0.0
request-progress: 3.0.0
- semver: 7.7.3
supports-color: 8.1.1
+ systeminformation: 5.30.5
tmp: 0.2.5
tree-kill: 1.2.2
untildify: 4.0.0
@@ -11069,9 +10828,6 @@ snapshots:
detect-indent@6.1.0: {}
- detect-libc@2.0.4:
- optional: true
-
detect-libc@2.1.2:
optional: true
@@ -11386,17 +11142,17 @@ snapshots:
extend@3.0.2: {}
- extract-react-intl-messages@4.1.1(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)):
+ extract-react-intl-messages@5.0.0(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)):
dependencies:
'@babel/core': 7.28.3
babel-plugin-react-intl: 7.9.4(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
+ file-entry-cache: 5.0.1
flat: 5.0.2
glob: 7.2.3
js-yaml: 3.14.1
load-json-file: 6.2.0
lodash.merge: 4.6.2
lodash.mergewith: 4.6.2
- lodash.pick: 4.4.0
meow: 6.1.1
mkdirp: 1.0.4
pify: 5.0.0
@@ -11464,6 +11220,10 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
+ file-entry-cache@5.0.1:
+ dependencies:
+ flat-cache: 2.0.1
+
file-entry-cache@7.0.2:
dependencies:
flat-cache: 3.2.0
@@ -11488,6 +11248,12 @@ snapshots:
mlly: 1.8.0
rollup: 4.53.3
+ flat-cache@2.0.1:
+ dependencies:
+ flatted: 2.0.2
+ rimraf: 2.6.3
+ write: 1.0.3
+
flat-cache@3.2.0:
dependencies:
flatted: 3.3.3
@@ -11496,6 +11262,8 @@ snapshots:
flat@5.0.2: {}
+ flatted@2.0.2: {}
+
flatted@3.3.3: {}
for-each@0.3.5:
@@ -11614,10 +11382,6 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
- getos@3.2.1:
- dependencies:
- async: 3.2.6
-
getpass@0.1.7:
dependencies:
assert-plus: 1.0.0
@@ -11752,6 +11516,11 @@ snapshots:
dependencies:
has-symbols: 1.1.0
+ hasha@5.2.2:
+ dependencies:
+ is-stream: 2.0.1
+ type-fest: 0.8.1
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -11871,9 +11640,6 @@ snapshots:
is-arrayish@0.2.1: {}
- is-arrayish@0.3.2:
- optional: true
-
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -12490,9 +12256,9 @@ snapshots:
jsonify@0.0.1: {}
- jsonwebtoken@9.0.2:
+ jsonwebtoken@9.0.3:
dependencies:
- jws: 3.2.2
+ jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
@@ -12517,15 +12283,15 @@ snapshots:
readable-stream: 2.3.8
setimmediate: 1.0.5
- jwa@1.4.2:
+ jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
- jws@3.2.2:
+ jws@4.0.1:
dependencies:
- jwa: 1.4.2
+ jwa: 2.0.1
safe-buffer: 5.2.1
kafkajs@2.2.4: {}
@@ -12543,8 +12309,6 @@ snapshots:
known-css-properties@0.36.0:
optional: true
- lazy-ass@1.6.0: {}
-
leven@3.1.0: {}
lie@3.3.0:
@@ -12637,8 +12401,6 @@ snapshots:
lodash.once@4.1.1: {}
- lodash.pick@4.4.0: {}
-
lodash.truncate@4.4.2: {}
lodash.uniq@4.5.0: {}
@@ -12862,11 +12624,11 @@ snapshots:
neo-async@2.6.2: {}
- next@15.5.9(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ next@15.5.10(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- '@next/env': 15.5.9
+ '@next/env': 15.5.10
'@swc/helpers': 0.5.15
- caniuse-lite: 1.0.30001741
+ caniuse-lite: 1.0.30001766
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -12881,29 +12643,30 @@ snapshots:
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
babel-plugin-react-compiler: 19.1.0-rc.2
- sharp: 0.34.3
+ sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- next@16.0.10(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ next@16.1.5(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- '@next/env': 16.0.10
+ '@next/env': 16.1.5
'@swc/helpers': 0.5.15
- caniuse-lite: 1.0.30001759
+ baseline-browser-mapping: 2.9.18
+ caniuse-lite: 1.0.30001766
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.3)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.0.10
- '@next/swc-darwin-x64': 16.0.10
- '@next/swc-linux-arm64-gnu': 16.0.10
- '@next/swc-linux-arm64-musl': 16.0.10
- '@next/swc-linux-x64-gnu': 16.0.10
- '@next/swc-linux-x64-musl': 16.0.10
- '@next/swc-win32-arm64-msvc': 16.0.10
- '@next/swc-win32-x64-msvc': 16.0.10
+ '@next/swc-darwin-arm64': 16.1.5
+ '@next/swc-darwin-x64': 16.1.5
+ '@next/swc-linux-arm64-gnu': 16.1.5
+ '@next/swc-linux-arm64-musl': 16.1.5
+ '@next/swc-linux-x64-gnu': 16.1.5
+ '@next/swc-linux-x64-musl': 16.1.5
+ '@next/swc-win32-arm64-msvc': 16.1.5
+ '@next/swc-win32-x64-msvc': 16.1.5
babel-plugin-react-compiler: 19.1.0-rc.2
sharp: 0.34.5
transitivePeerDependencies:
@@ -13677,7 +13440,7 @@ snapshots:
pure-rand@7.0.1: {}
- qs@6.14.0:
+ qs@6.14.1:
dependencies:
side-channel: 1.1.0
@@ -13724,7 +13487,7 @@ snapshots:
'@react-types/grid': 3.3.6(react@19.2.3)
'@react-types/shared': 3.32.1(react@19.2.3)
'@react-types/table': 3.13.4(react@19.2.3)
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
client-only: 0.0.1
react: 19.2.3
react-aria: 3.44.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -14015,6 +13778,10 @@ snapshots:
rfdc@1.4.1: {}
+ rimraf@2.6.3:
+ dependencies:
+ glob: 7.2.3
+
rimraf@3.0.2:
dependencies:
glob: 7.2.3
@@ -14180,36 +13947,6 @@ snapshots:
setimmediate@1.0.5: {}
- sharp@0.34.3:
- dependencies:
- color: 4.2.3
- detect-libc: 2.0.4
- semver: 7.7.3
- optionalDependencies:
- '@img/sharp-darwin-arm64': 0.34.3
- '@img/sharp-darwin-x64': 0.34.3
- '@img/sharp-libvips-darwin-arm64': 1.2.0
- '@img/sharp-libvips-darwin-x64': 1.2.0
- '@img/sharp-libvips-linux-arm': 1.2.0
- '@img/sharp-libvips-linux-arm64': 1.2.0
- '@img/sharp-libvips-linux-ppc64': 1.2.0
- '@img/sharp-libvips-linux-s390x': 1.2.0
- '@img/sharp-libvips-linux-x64': 1.2.0
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
- '@img/sharp-libvips-linuxmusl-x64': 1.2.0
- '@img/sharp-linux-arm': 0.34.3
- '@img/sharp-linux-arm64': 0.34.3
- '@img/sharp-linux-ppc64': 0.34.3
- '@img/sharp-linux-s390x': 0.34.3
- '@img/sharp-linux-x64': 0.34.3
- '@img/sharp-linuxmusl-arm64': 0.34.3
- '@img/sharp-linuxmusl-x64': 0.34.3
- '@img/sharp-wasm32': 0.34.3
- '@img/sharp-win32-arm64': 0.34.3
- '@img/sharp-win32-ia32': 0.34.3
- '@img/sharp-win32-x64': 0.34.3
- optional: true
-
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
@@ -14288,11 +14025,6 @@ snapshots:
signal-exit@4.1.0: {}
- simple-swizzle@0.2.2:
- dependencies:
- is-arrayish: 0.3.2
- optional: true
-
sisteransi@1.0.5: {}
slash@3.0.0: {}
@@ -14617,6 +14349,8 @@ snapshots:
csso: 5.0.5
picocolors: 1.1.1
+ systeminformation@5.30.5: {}
+
table@6.9.0:
dependencies:
ajv: 8.17.1
@@ -15038,6 +14772,10 @@ snapshots:
sort-keys: 4.2.0
write-file-atomic: 3.0.3
+ write@1.0.3:
+ dependencies:
+ mkdirp: 0.5.6
+
xtend@4.0.2: {}
y18n@5.0.8: {}
diff --git a/prisma/migrations/15_add_share/migration.sql b/prisma/migrations/15_add_share/migration.sql
new file mode 100644
index 00000000..89aece1e
--- /dev/null
+++ b/prisma/migrations/15_add_share/migration.sql
@@ -0,0 +1,40 @@
+-- CreateTable
+CREATE TABLE "share" (
+ "share_id" UUID NOT NULL,
+ "entity_id" UUID NOT NULL,
+ "name" VARCHAR(200) NOT NULL,
+ "share_type" INTEGER NOT NULL,
+ "slug" VARCHAR(100) NOT NULL,
+ "parameters" JSONB NOT NULL,
+ "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMPTZ(6),
+
+ CONSTRAINT "share_pkey" PRIMARY KEY ("share_id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
+
+-- CreateIndex
+CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
+
+-- MigrateData
+INSERT INTO "share" (share_id, entity_id, name, share_type, slug, parameters, created_at)
+SELECT gen_random_uuid(),
+ website_id,
+ name,
+ 1,
+ share_id,
+ '{"overview":true}'::jsonb,
+ now()
+FROM "website"
+WHERE share_id IS NOT NULL;
+
+-- DropIndex
+DROP INDEX "website_share_id_idx";
+
+-- DropIndex
+DROP INDEX "website_share_id_key";
+
+-- AlterTable
+ALTER TABLE "website" DROP COLUMN "share_id";
diff --git a/prisma/migrations/16_boards/migration.sql b/prisma/migrations/16_boards/migration.sql
new file mode 100644
index 00000000..ad8ee172
--- /dev/null
+++ b/prisma/migrations/16_boards/migration.sql
@@ -0,0 +1,30 @@
+-- CreateTable
+CREATE TABLE "board" (
+ "board_id" UUID NOT NULL,
+ "type" VARCHAR(50) NOT NULL,
+ "name" VARCHAR(200) NOT NULL,
+ "description" VARCHAR(500) NOT NULL,
+ "parameters" JSONB NOT NULL,
+ "slug" VARCHAR(100) NOT NULL,
+ "user_id" UUID,
+ "team_id" UUID,
+ "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMPTZ(6),
+
+ CONSTRAINT "board_pkey" PRIMARY KEY ("board_id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "board_slug_key" ON "board"("slug");
+
+-- CreateIndex
+CREATE INDEX "board_slug_idx" ON "board"("slug");
+
+-- CreateIndex
+CREATE INDEX "board_user_id_idx" ON "board"("user_id");
+
+-- CreateIndex
+CREATE INDEX "board_team_id_idx" ON "board"("team_id");
+
+-- CreateIndex
+CREATE INDEX "board_created_at_idx" ON "board"("created_at");
\ No newline at end of file
diff --git a/prisma/migrations/17_remove_duplicate_key/migration.sql b/prisma/migrations/17_remove_duplicate_key/migration.sql
new file mode 100644
index 00000000..75f7191e
--- /dev/null
+++ b/prisma/migrations/17_remove_duplicate_key/migration.sql
@@ -0,0 +1,29 @@
+-- DropIndex
+DROP INDEX "link_link_id_key";
+
+-- DropIndex
+DROP INDEX "pixel_pixel_id_key";
+
+-- DropIndex
+DROP INDEX "report_report_id_key";
+
+-- DropIndex
+DROP INDEX "revenue_revenue_id_key";
+
+-- DropIndex
+DROP INDEX "segment_segment_id_key";
+
+-- DropIndex
+DROP INDEX "session_session_id_key";
+
+-- DropIndex
+DROP INDEX "team_team_id_key";
+
+-- DropIndex
+DROP INDEX "team_user_team_user_id_key";
+
+-- DropIndex
+DROP INDEX "user_user_id_key";
+
+-- DropIndex
+DROP INDEX "website_website_id_key";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index aeb11648..e58ebd0b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,7 +11,7 @@ datasource db {
}
model User {
- id String @id @unique @map("user_id") @db.Uuid
+ id String @id() @map("user_id") @db.Uuid
username String @unique @db.VarChar(255)
password String @db.VarChar(60)
role String @map("role") @db.VarChar(50)
@@ -27,12 +27,13 @@ model User {
pixels Pixel[] @relation("user")
teams TeamUser[]
reports Report[]
+ boards Board[] @relation("user")
@@map("user")
}
model Session {
- id String @id @unique @map("session_id") @db.Uuid
+ id String @id() @map("session_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
@@ -64,10 +65,9 @@ model Session {
}
model Website {
- id String @id @unique @map("website_id") @db.Uuid
+ id String @id() @map("website_id") @db.Uuid
name String @db.VarChar(100)
domain String? @db.VarChar(500)
- shareId String? @unique @map("share_id") @db.VarChar(50)
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
userId String? @map("user_id") @db.Uuid
teamId String? @map("team_id") @db.Uuid
@@ -88,7 +88,6 @@ model Website {
@@index([userId])
@@index([teamId])
@@index([createdAt])
- @@index([shareId])
@@index([createdBy])
@@map("website")
}
@@ -187,7 +186,7 @@ model SessionData {
}
model Team {
- id String @id() @unique() @map("team_id") @db.Uuid
+ id String @id() @map("team_id") @db.Uuid
name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50)
logoUrl String? @map("logo_url") @db.VarChar(2183)
@@ -199,13 +198,14 @@ model Team {
members TeamUser[]
links Link[]
pixels Pixel[]
+ boards Board[]
@@index([accessCode])
@@map("team")
}
model TeamUser {
- id String @id() @unique() @map("team_user_id") @db.Uuid
+ id String @id() @map("team_user_id") @db.Uuid
teamId String @map("team_id") @db.Uuid
userId String @map("user_id") @db.Uuid
role String @db.VarChar(50)
@@ -221,7 +221,7 @@ model TeamUser {
}
model Report {
- id String @id() @unique() @map("report_id") @db.Uuid
+ id String @id() @map("report_id") @db.Uuid
userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
type String @db.VarChar(50)
@@ -242,7 +242,7 @@ model Report {
}
model Segment {
- id String @id() @unique() @map("segment_id") @db.Uuid
+ id String @id() @map("segment_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
type String @db.VarChar(50)
name String @db.VarChar(200)
@@ -257,7 +257,7 @@ model Segment {
}
model Revenue {
- id String @id() @unique() @map("revenue_id") @db.Uuid
+ id String @id() @map("revenue_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
eventId String @map("event_id") @db.Uuid
@@ -277,7 +277,7 @@ model Revenue {
}
model Link {
- id String @id() @unique() @map("link_id") @db.Uuid
+ id String @id() @map("link_id") @db.Uuid
name String @db.VarChar(100)
url String @db.VarChar(500)
slug String @unique() @db.VarChar(100)
@@ -298,7 +298,7 @@ model Link {
}
model Pixel {
- id String @id() @unique() @map("pixel_id") @db.Uuid
+ id String @id() @map("pixel_id") @db.Uuid
name String @db.VarChar(100)
slug String @unique() @db.VarChar(100)
userId String? @map("user_id") @db.Uuid
@@ -316,3 +316,39 @@ model Pixel {
@@index([createdAt])
@@map("pixel")
}
+
+model Board {
+ id String @id() @map("board_id") @db.Uuid
+ type String @db.VarChar(50)
+ name String @db.VarChar(200)
+ description String @db.VarChar(500)
+ parameters Json
+ slug String @unique() @db.VarChar(100)
+ userId String? @map("user_id") @db.Uuid
+ teamId String? @map("team_id") @db.Uuid
+ createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
+ updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
+
+ user User? @relation("user", fields: [userId], references: [id])
+ team Team? @relation(fields: [teamId], references: [id])
+
+ @@index([slug])
+ @@index([userId])
+ @@index([teamId])
+ @@index([createdAt])
+ @@map("board")
+}
+
+model Share {
+ id String @id() @map("share_id") @db.Uuid
+ entityId String @map("entity_id") @db.Uuid
+ name String @db.VarChar(200)
+ shareType Int @map("share_type") @db.Integer
+ slug String @unique() @db.VarChar(100)
+ parameters Json
+ createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
+ updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
+
+ @@index([entityId])
+ @@map("share")
+}
diff --git a/public/images/country/t1.png b/public/images/country/t1.png
new file mode 100644
index 00000000..c45ed0fe
Binary files /dev/null and b/public/images/country/t1.png differ
diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json
index b3d2f3c0..a4ad51fa 100644
--- a/public/intl/messages/zh-CN.json
+++ b/public/intl/messages/zh-CN.json
@@ -5,6 +5,18 @@
"value": "访问代码"
}
],
+ "label.account": [
+ {
+ "type": 0,
+ "value": "账户"
+ }
+ ],
+ "label.action": [
+ {
+ "type": 0,
+ "value": "行为"
+ }
+ ],
"label.actions": [
{
"type": 0,
@@ -35,12 +47,24 @@
"value": "添加描述"
}
],
+ "label.add-link": [
+ {
+ "type": 0,
+ "value": "添加链接"
+ }
+ ],
"label.add-member": [
{
"type": 0,
"value": "添加成员"
}
],
+ "label.add-pixel": [
+ {
+ "type": 0,
+ "value": "添加像素"
+ }
+ ],
"label.add-step": [
{
"type": 0,
@@ -83,12 +107,24 @@
"value": "所有时间段"
}
],
+ "label.analysis": [
+ {
+ "type": 0,
+ "value": "分析"
+ }
+ ],
"label.analytics": [
{
"type": 0,
"value": "分析"
}
],
+ "label.application": [
+ {
+ "type": 0,
+ "value": "应用"
+ }
+ ],
"label.apply": [
{
"type": 0,
@@ -107,6 +143,12 @@
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
}
],
+ "label.audience": [
+ {
+ "type": 0,
+ "value": "受众"
+ }
+ ],
"label.average": [
{
"type": 0,
@@ -125,6 +167,12 @@
"value": "之前"
}
],
+ "label.behavior": [
+ {
+ "type": 0,
+ "value": "行为"
+ }
+ ],
"label.boards": [
{
"type": 0,
@@ -173,12 +221,24 @@
"value": "修改密码"
}
],
+ "label.channel": [
+ {
+ "type": 0,
+ "value": "渠道"
+ }
+ ],
"label.channels": [
{
"type": 0,
"value": "渠道"
}
],
+ "label.chart": [
+ {
+ "type": 0,
+ "value": "图表"
+ }
+ ],
"label.cities": [
{
"type": 0,
@@ -203,6 +263,12 @@
"value": "队列"
}
],
+ "label.cohorts": [
+ {
+ "type": 0,
+ "value": "队列"
+ }
+ ],
"label.compare": [
{
"type": 0,
@@ -317,6 +383,12 @@
"value": "创建者"
}
],
+ "label.criteria": [
+ {
+ "type": 0,
+ "value": "条件"
+ }
+ ],
"label.currency": [
{
"type": 0,
@@ -419,6 +491,12 @@
"value": "台式机"
}
],
+ "label.destination-url": [
+ {
+ "type": 0,
+ "value": "目标URL"
+ }
+ ],
"label.details": [
{
"type": 0,
@@ -455,6 +533,12 @@
"value": "唯一ID"
}
],
+ "label.documentation": [
+ {
+ "type": 0,
+ "value": "文档"
+ }
+ ],
"label.does-not-contain": [
{
"type": 0,
@@ -479,6 +563,12 @@
"value": "域名"
}
],
+ "label.download": [
+ {
+ "type": 0,
+ "value": "下载"
+ }
+ ],
"label.dropoff": [
{
"type": 0,
@@ -506,7 +596,7 @@
"label.email": [
{
"type": 0,
- "value": "Email"
+ "value": "邮箱"
}
],
"label.enable-share-url": [
@@ -527,6 +617,12 @@
"value": "入口 URL"
}
],
+ "label.environment": [
+ {
+ "type": 0,
+ "value": "环境"
+ }
+ ],
"label.event": [
{
"type": 0,
@@ -671,6 +767,12 @@
"value": "分组"
}
],
+ "label.growth": [
+ {
+ "type": 0,
+ "value": "增长"
+ }
+ ],
"label.hostname": [
{
"type": 0,
@@ -701,6 +803,12 @@
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
}
],
+ "label.invalid-url": [
+ {
+ "type": 0,
+ "value": "无效URL"
+ }
+ ],
"label.is": [
{
"type": 0,
@@ -863,12 +971,24 @@
"value": "少于等于"
}
],
+ "label.link": [
+ {
+ "type": 0,
+ "value": "链接"
+ }
+ ],
"label.links": [
{
"type": 0,
"value": "链接"
}
],
+ "label.location": [
+ {
+ "type": 0,
+ "value": "位置"
+ }
+ ],
"label.login": [
{
"type": 0,
@@ -1020,7 +1140,7 @@
"label.online": [
{
"type": 0,
- "value": "Online"
+ "value": "在线"
}
],
"label.organic-search": [
@@ -1165,6 +1285,12 @@
"value": "路径"
}
],
+ "label.pixel": [
+ {
+ "type": 0,
+ "value": "像素"
+ }
+ ],
"label.pixels": [
{
"type": 0,
@@ -1185,6 +1311,12 @@
"value": " 提供支持"
}
],
+ "label.preferences": [
+ {
+ "type": 0,
+ "value": "偏好"
+ }
+ ],
"label.previous": [
{
"type": 0,
@@ -1209,6 +1341,12 @@
"value": "个人资料"
}
],
+ "label.profiles": [
+ {
+ "type": 0,
+ "value": "个人资料"
+ }
+ ],
"label.properties": [
{
"type": 0,
@@ -1248,7 +1386,7 @@
"label.referral": [
{
"type": 0,
- "value": "Referral"
+ "value": "来源"
}
],
"label.referrer": [
@@ -1371,6 +1509,24 @@
"value": "保存"
}
],
+ "label.save-cohort": [
+ {
+ "type": 0,
+ "value": "保存为群组"
+ }
+ ],
+ "label.save-segment": [
+ {
+ "type": 0,
+ "value": "保存为细分"
+ }
+ ],
+ "label.screen": [
+ {
+ "type": 0,
+ "value": "屏幕"
+ }
+ ],
"label.screens": [
{
"type": 0,
@@ -1383,6 +1539,18 @@
"value": "搜索"
}
],
+ "label.segment": [
+ {
+ "type": 0,
+ "value": "细分"
+ }
+ ],
+ "label.segments": [
+ {
+ "type": 0,
+ "value": "细分"
+ }
+ ],
"label.select": [
{
"type": 0,
@@ -1485,6 +1653,24 @@
"value": "总和"
}
],
+ "label.support": [
+ {
+ "type": 0,
+ "value": "支持"
+ }
+ ],
+ "label.switch-account": [
+ {
+ "type": 0,
+ "value": "切换账户"
+ }
+ ],
+ "label.table": [
+ {
+ "type": 0,
+ "value": "表格"
+ }
+ ],
"label.tablet": [
{
"type": 0,
@@ -1635,6 +1821,12 @@
"value": "跟踪代码"
}
],
+ "label.traffic": [
+ {
+ "type": 0,
+ "value": "流量"
+ }
+ ],
"label.transactions": [
{
"type": 0,
@@ -1846,7 +2038,7 @@
"message.bad-request": [
{
"type": 0,
- "value": "Bad request"
+ "value": "请求错误"
}
],
"message.collected-data": [
@@ -1946,7 +2138,7 @@
"message.forbidden": [
{
"type": 0,
- "value": "Forbidden"
+ "value": "禁止访问"
}
],
"message.go-to-settings": [
@@ -2046,13 +2238,13 @@
"message.not-found": [
{
"type": 0,
- "value": "Not found"
+ "value": "未找到"
}
],
"message.nothing-selected": [
{
"type": 0,
- "value": "Nothing selected."
+ "value": "未选择"
}
],
"message.page-not-found": [
@@ -2090,7 +2282,7 @@
"message.sever-error": [
{
"type": 0,
- "value": "Server error"
+ "value": "服务器错误"
}
],
"message.share-url": [
@@ -2158,7 +2350,7 @@
"message.unauthorized": [
{
"type": 0,
- "value": "Unauthorized"
+ "value": "未授权"
}
],
"message.user-deleted": [
diff --git a/public/iso-3166-2.json b/public/iso-3166-2.json
index 347313d7..2b3b5a80 100644
--- a/public/iso-3166-2.json
+++ b/public/iso-3166-2.json
@@ -6,13 +6,13 @@
"AD-06": "Sant Julia de Loria",
"AD-07": "Andorra la Vella",
"AD-08": "Escaldes-Engordany",
- "AE-AJ": "'Ajman",
- "AE-AZ": "Abu Zaby",
- "AE-DU": "Dubayy",
- "AE-FU": "Al Fujayrah",
- "AE-RK": "Ra's al Khaymah",
- "AE-SH": "Ash Shariqah",
- "AE-UQ": "Umm al Qaywayn",
+ "AE-AJ": "Ajman",
+ "AE-AZ": "Abu Dhabi",
+ "AE-DU": "Dubai",
+ "AE-FU": "Al Fujairah",
+ "AE-RK": "Ras al Khaimah",
+ "AE-SH": "Sharjah",
+ "AE-UQ": "Umm al Quwain",
"AF-BAL": "Balkh",
"AF-BAM": "Bamyan",
"AF-BDG": "Badghis",
diff --git a/scripts/build-geo.js b/scripts/build-geo.js
index a83caa6c..e36b097c 100644
--- a/scripts/build-geo.js
+++ b/scripts/build-geo.js
@@ -3,7 +3,7 @@ import 'dotenv/config';
import fs from 'node:fs';
import path from 'node:path';
import https from 'https';
-import tar from 'tar';
+import { list } from 'tar';
import zlib from 'zlib';
if (process.env.VERCEL && !process.env.BUILD_GEO) {
@@ -40,7 +40,7 @@ const isDirectMmdb = url.endsWith('.mmdb');
const downloadCompressed = url =>
new Promise(resolve => {
https.get(url, res => {
- resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
+ resolve(res.pipe(zlib.createGunzip({})).pipe(list()));
});
});
diff --git a/src/app/(main)/admin/teams/AdminTeamsPage.tsx b/src/app/(main)/admin/teams/AdminTeamsPage.tsx
index 41e6f4af..7905f7f6 100644
--- a/src/app/(main)/admin/teams/AdminTeamsPage.tsx
+++ b/src/app/(main)/admin/teams/AdminTeamsPage.tsx
@@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
+import { TeamsAddButton } from '../../teams/TeamsAddButton';
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
export function AdminTeamsPage() {
const { formatMessage, labels } = useMessages();
+ const handleSave = () => {};
+
return (
-
+
+
+
diff --git a/src/app/(main)/admin/users/UserAddForm.tsx b/src/app/(main)/admin/users/UserAddForm.tsx
index 6c365510..84b8399c 100644
--- a/src/app/(main)/admin/users/UserAddForm.tsx
+++ b/src/app/(main)/admin/users/UserAddForm.tsx
@@ -10,6 +10,7 @@ import {
TextField,
} from '@umami/react-zen';
import { useMessages, useUpdateQuery } from '@/components/hooks';
+import { messages } from '@/components/messages';
import { ROLES } from '@/lib/constants';
export function UserAddForm({ onSave, onClose }) {
@@ -37,7 +38,10 @@ export function UserAddForm({ onSave, onClose }) {
diff --git a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx
index 28bf030f..68aa7f6e 100644
--- a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx
+++ b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx
@@ -30,7 +30,11 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
};
return (
-
+
+
+
+
);
}
diff --git a/src/app/(main)/settings/preferences/VersionSetting.tsx b/src/app/(main)/settings/preferences/VersionSetting.tsx
new file mode 100644
index 00000000..afca1de6
--- /dev/null
+++ b/src/app/(main)/settings/preferences/VersionSetting.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import { Text } from '@umami/react-zen';
+import { CURRENT_VERSION } from '@/lib/constants';
+
+export function VersionSetting() {
+ return {CURRENT_VERSION};
+}
diff --git a/src/app/(main)/teams/TeamAddForm.tsx b/src/app/(main)/teams/TeamAddForm.tsx
index c95259f4..3b827776 100644
--- a/src/app/(main)/teams/TeamAddForm.tsx
+++ b/src/app/(main)/teams/TeamAddForm.tsx
@@ -7,8 +7,17 @@ import {
TextField,
} from '@umami/react-zen';
import { useMessages, useUpdateQuery } from '@/components/hooks';
+import { UserSelect } from '@/components/input/UserSelect';
-export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
+export function TeamAddForm({
+ onSave,
+ onClose,
+ isAdmin,
+}: {
+ onSave: () => void;
+ onClose: () => void;
+ isAdmin: boolean;
+}) {
const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
@@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
+ {isAdmin && (
+
+
+
+ )}
diff --git a/src/app/(main)/teams/TeamsMemberAddButton.tsx b/src/app/(main)/teams/TeamsMemberAddButton.tsx
new file mode 100644
index 00000000..f1bbf258
--- /dev/null
+++ b/src/app/(main)/teams/TeamsMemberAddButton.tsx
@@ -0,0 +1,40 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
+import { useMessages, useModified } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { messages } from '@/components/messages';
+import { TeamMemberAddForm } from './TeamMemberAddForm';
+
+export function TeamsMemberAddButton({
+ teamId,
+ onSave,
+}: {
+ teamId: string;
+ onSave?: () => void;
+ isAdmin?: boolean;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { toast } = useToast();
+ const { touch } = useModified();
+
+ const handleSave = async () => {
+ toast(formatMessage(messages.saved));
+ touch('teams:members');
+ onSave?.();
+ };
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx
index 3ddbe000..4bbb8905 100644
--- a/src/app/(main)/teams/[teamId]/TeamSettings.tsx
+++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx
@@ -1,10 +1,12 @@
-import { Column } from '@umami/react-zen';
+import { Column, Heading, Row } from '@umami/react-zen';
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
-import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
+import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { Users } from '@/components/icons';
+import { labels } from '@/components/messages';
import { ROLES } from '@/lib/constants';
+import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
import { TeamEditForm } from './TeamEditForm';
import { TeamManage } from './TeamManage';
import { TeamMembersDataTable } from './TeamMembersDataTable';
@@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
const team: any = useTeam();
const { user } = useLoginQuery();
const { pathname } = useNavigation();
+ const { formatMessage } = useMessages();
const isAdmin = pathname.includes('/admin');
@@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
+
+ {formatMessage(labels.members)}
+ {isAdmin && }
+
{isTeamOwner && (
diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx
index 31de7047..6f3548a9 100644
--- a/src/app/(main)/websites/WebsitesPage.tsx
+++ b/src/app/(main)/websites/WebsitesPage.tsx
@@ -3,22 +3,31 @@ 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, useNavigation } from '@/components/hooks';
+import { useLoginQuery, useMessages, useNavigation, useTeamMembersQuery } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
import { WebsiteAddButton } from './WebsiteAddButton';
import { WebsitesDataTable } from './WebsitesDataTable';
export function WebsitesPage() {
+ const { user } = useLoginQuery();
const { teamId } = useNavigation();
const { formatMessage, labels } = useMessages();
+ const { data } = useTeamMembersQuery(teamId);
+
+ const showActions =
+ (teamId &&
+ data?.data.filter(team => team.userId === user.id && team.role !== ROLES.teamViewOnly)
+ .length > 0) ||
+ (!teamId && user.role !== ROLES.viewOnly);
return (
-
+ {showActions && }
-
+
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
index e336a3db..d81519d7 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
@@ -1,6 +1,6 @@
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { useMessages, useResultQuery } from '@/components/hooks';
+import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
import { File, User } from '@/components/icons';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
@@ -20,6 +20,8 @@ type FunnelResult = {
export function Funnel({ id, name, type, parameters, websiteId }) {
const { formatMessage, labels } = useMessages();
+ const { pathname } = useNavigation();
+ const isSharePage = pathname.includes('/share/');
const { data, error, isLoading } = useResultQuery(type, {
websiteId,
...parameters,
@@ -36,21 +38,22 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
-
-
- {({ close }) => {
- return (
-
- );
- }}
-
-
+ {!isSharePage && (
+
+
+ {({ close }) => {
+ return (
+
+ );
+ }}
+
+
+ )}
{data?.map(
(
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx
index 57bce52f..a56917b7 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx
@@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
import { SectionHeader } from '@/components/common/SectionHeader';
-import { useDateRange, useReportsQuery } from '@/components/hooks';
+import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
import { Funnel } from './Funnel';
import { FunnelAddButton } from './FunnelAddButton';
@@ -13,13 +13,17 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate, endDate },
} = useDateRange();
+ const { pathname } = useNavigation();
+ const isSharePage = pathname.includes('/share/');
return (
-
-
-
+ {!isSharePage && (
+
+
+
+ )}
{data && (
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx
index b6c4a11d..1d0b96e5 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx
@@ -1,6 +1,6 @@
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { useMessages, useResultQuery } from '@/components/hooks';
+import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
import { File, User } from '@/components/icons';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { Lightning } from '@/components/svg';
@@ -25,6 +25,8 @@ export type GoalData = { num: number; total: number };
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
const { formatMessage, labels } = useMessages();
+ const { pathname } = useNavigation();
+ const isSharePage = pathname.includes('/share/');
const { data, error, isLoading, isFetching } = useResultQuery(type, {
websiteId,
startDate,
@@ -45,21 +47,23 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
-
-
- {({ close }) => {
- return (
-
- );
- }}
-
-
+ {!isSharePage && (
+
+
+ {({ close }) => {
+ return (
+
+ );
+ }}
+
+
+ )}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx
index ff7b49fb..fe4550d6 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx
@@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
import { SectionHeader } from '@/components/common/SectionHeader';
-import { useDateRange, useReportsQuery } from '@/components/hooks';
+import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
import { Goal } from './Goal';
import { GoalAddButton } from './GoalAddButton';
@@ -13,13 +13,17 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate, endDate },
} = useDateRange();
+ const { pathname } = useNavigation();
+ const isSharePage = pathname.includes('/share/');
return (
-
-
-
+ {!isSharePage && (
+
+
+
+ )}
{data && (
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx
index 3327a425..1b893d27 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx
@@ -21,9 +21,15 @@ export interface JourneyProps {
steps: number;
startStep?: string;
endStep?: string;
+ view: string;
}
-export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
+const EVENT_TYPES = {
+ views: 1,
+ events: 2,
+};
+
+export function Journey({ websiteId, steps, startStep, endStep, view }: JourneyProps) {
const [selectedNode, setSelectedNode] = useState(null);
const [activeNode, setActiveNode] = useState(null);
const { formatMessage, labels } = useMessages();
@@ -32,6 +38,8 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
steps,
startStep,
endStep,
+ view,
+ eventType: EVENT_TYPES[view],
});
useEscapeKey(() => setSelectedNode(null));
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
index 14b8341d..f1a8976f 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
@@ -1,9 +1,10 @@
'use client';
-import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
+import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
import { useState } from 'react';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel';
import { useDateRange, useMessages } from '@/components/hooks';
+import { FilterButtons } from '@/components/input/FilterButtons';
import { Journey } from './Journey';
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
@@ -14,10 +15,26 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate, endDate },
} = useDateRange();
+ const [view, setView] = useState('all');
const [steps, setSteps] = useState(DEFAULT_STEP);
const [startStep, setStartStep] = useState('');
const [endStep, setEndStep] = useState('');
+ const buttons = [
+ {
+ id: 'all',
+ label: formatMessage(labels.all),
+ },
+ {
+ id: 'views',
+ label: formatMessage(labels.views),
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ },
+ ];
+
return (
@@ -52,6 +69,9 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
/>
+
+
+
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
index 0e782a16..faee8b9a 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
@@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { renderDateLabels } from '@/lib/charts';
-import { CHART_COLORS } from '@/lib/constants';
+import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
import { generateTimeSeries } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
+import { getItem, setItem } from '@/lib/storage';
export interface RevenueProps {
websiteId: string;
@@ -24,7 +25,15 @@ export interface RevenueProps {
}
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
- const [currency, setCurrency] = useState('USD');
+ const [currency, setCurrency] = useState(
+ getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
+ );
+
+ const handleCurrencyChange = (value: string) => {
+ setCurrency(value);
+ setItem(CURRENCY_CONFIG, value);
+ };
+
const { formatMessage, labels } = useMessages();
const { locale, dateLocale } = useLocale();
const { countryNames } = useCountryNames(locale);
@@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
return (
-
+
{data && (
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
index b2ea2a83..896c733a 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
@@ -21,30 +21,28 @@ export function WebsiteChart({
const { pageviews, sessions, compare } = (data || {}) as any;
const chartData = useMemo(() => {
- if (data) {
- const result = {
- pageviews,
- sessions,
- };
+ if (!data) {
+ return { pageviews: [], sessions: [] };
+ }
- if (compare) {
- result.compare = {
- pageviews: result.pageviews.map(({ x }, i) => ({
+ return {
+ pageviews,
+ sessions,
+ ...(compare && {
+ compare: {
+ pageviews: pageviews.map(({ x }, i) => ({
x,
y: compare.pageviews[i]?.y,
d: compare.pageviews[i]?.x,
})),
- sessions: result.sessions.map(({ x }, i) => ({
+ sessions: sessions.map(({ x }, i) => ({
x,
y: compare.sessions[i]?.y,
d: compare.sessions[i]?.x,
})),
- };
- }
-
- return result;
- }
- return { pageviews: [], sessions: [] };
+ },
+ }),
+ };
}, [data, startDate, endDate, unit]);
return (
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
index 29c3954f..4bac4ff6 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
@@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({
path: updateParams({ view: 'hostname' }),
icon: ,
},
+ {
+ id: 'distinctId',
+ label: formatMessage(labels.distinctId),
+ path: updateParams({ view: 'distinctId' }),
+ icon: ,
+ },
{
id: 'tag',
label: formatMessage(labels.tag),
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
index 7dd1d771..1dee8022 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
@@ -1,14 +1,18 @@
import { Icon, Row, Text } from '@umami/react-zen';
-import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
import { Favicon } from '@/components/common/Favicon';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
-import { Edit, Share } from '@/components/icons';
-import { DialogButton } from '@/components/input/DialogButton';
+import { Edit } from '@/components/icons';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
-export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
+export function WebsiteHeader({
+ showActions,
+ allowLink = true,
+}: {
+ showActions?: boolean;
+ allowLink?: boolean;
+}) {
const website = useWebsite();
const { renderUrl, pathname } = useNavigation();
const isSettings = pathname.endsWith('/settings');
@@ -23,35 +27,20 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
}
- titleHref={renderUrl(`/websites/${website.id}`, false)}
+ titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
>
{showActions && (
-
-
-
-
-
-
- {formatMessage(labels.edit)}
-
-
+
+
+
+
+ {formatMessage(labels.edit)}
+
)}
);
}
-
-const ShareButton = ({ websiteId, shareId }) => {
- const { formatMessage, labels } = useMessages();
-
- return (
- } label={formatMessage(labels.share)} width="800px">
- {({ close }) => {
- return ;
- }}
-
- );
-};
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx
index 30189534..132d3b14 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx
@@ -10,7 +10,7 @@ import {
} from '@umami/react-zen';
import { Fragment } from 'react';
import { useMessages, useNavigation } from '@/components/hooks';
-import { Edit, More, Share } from '@/components/icons';
+import { Edit, MoreHorizontal, Share } from '@/components/icons';
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
@@ -33,7 +33,7 @@ export function WebsiteMenu({ websiteId }: { websiteId: string }) {
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
index 6c91ba6d..605ee385 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
@@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format';
export function WebsiteMetricsBar({
websiteId,
+ compareMode,
}: {
websiteId: string;
showChange?: boolean;
compareMode?: boolean;
}) {
- const { isAllTime } = useDateRange();
+ const { isAllTime, dateCompare } = useDateRange();
const { formatMessage, labels, getErrorMessage } = useMessages();
- const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({
+ websiteId,
+ compare: compareMode ? dateCompare?.compare : undefined,
+ });
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
index ad05b706..9f72c303 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
@@ -29,6 +29,7 @@ export function WebsiteNav({
event: undefined,
compare: undefined,
view: undefined,
+ unit: undefined,
});
const items = [
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
index f587e112..5acc9e68 100644
--- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
@@ -1,7 +1,8 @@
'use client';
-import { Column } from '@umami/react-zen';
+import { Column, Row } from '@umami/react-zen';
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
import { Panel } from '@/components/common/Panel';
+import { UnitFilter } from '@/components/input/UnitFilter';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteControls } from './WebsiteControls';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
@@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
+
+
+
diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
index bca8d244..32d641b0 100644
--- a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
@@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
return (
-
+
diff --git a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx
index 13c05160..4daf17fc 100644
--- a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx
+++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx
@@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
label: formatMessage(labels.hostname),
path: renderPath('hostname'),
},
+ {
+ id: 'distinctId',
+ label: formatMessage(labels.distinctId),
+ path: renderPath('distinctId'),
+ },
{
id: 'tag',
label: formatMessage(labels.tags),
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
index 55ec0403..f62d8a4c 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
@@ -1,12 +1,18 @@
'use client';
import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
-import { type Key, useState } from 'react';
+import locale from 'date-fns/locale/af';
+import { type Key, useMemo, useState } from 'react';
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
+import { useEventStatsQuery } from '@/components/hooks/queries/useEventStatsQuery';
import { EventsChart } from '@/components/metrics/EventsChart';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { formatLongNumber } from '@/lib/format';
import { getItem, setItem } from '@/lib/storage';
import { EventProperties } from './EventProperties';
import { EventsDataTable } from './EventsDataTable';
@@ -15,16 +21,61 @@ const KEY_NAME = 'umami.events.tab';
export function EventsPage({ websiteId }) {
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
- const { formatMessage, labels } = useMessages();
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+ const { data, isLoading, isFetching, error } = useEventStatsQuery({
+ websiteId,
+ });
const handleSelect = (value: Key) => {
setItem(KEY_NAME, value);
setTab(value);
};
+ const metrics = useMemo(() => {
+ if (!data) return [];
+
+ const { events, visitors, visits, uniqueEvents } = data || {};
+
+ return [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: events,
+ label: formatMessage(labels.events),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: uniqueEvents,
+ label: formatMessage(labels.uniqueEvents),
+ formatValue: formatLongNumber,
+ },
+ ] as any;
+ }, [data, locale]);
+
return (
+
+
+ {metrics?.map(({ label, value, formatValue }) => {
+ return ;
+ })}
+
+
handleSelect(key)}>
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
index 7fb2eb41..41c2b1e8 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
@@ -25,6 +25,19 @@ export function EventsTable(props: DataTableProps) {
const { updateParams } = useNavigation();
const { formatValue } = useFormat();
+ const renderLink = (label: string, hostname: string) => {
+ return (
+
+ {label}
+
+ );
+ };
+
return (
@@ -43,7 +56,7 @@ export function EventsTable(props: DataTableProps) {
title={row.eventName || row.urlPath}
truncate
>
- {row.eventName || row.urlPath}
+ {row.eventName || renderLink(row.urlPath, row.hostname)}
{row.hasData > 0 && }
diff --git a/src/app/(main)/websites/[websiteId]/layout.tsx b/src/app/(main)/websites/[websiteId]/layout.tsx
index 67595e9d..b12ff950 100644
--- a/src/app/(main)/websites/[websiteId]/layout.tsx
+++ b/src/app/(main)/websites/[websiteId]/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
+import { getWebsite } from '@/queries/prisma';
export default async function ({
children,
@@ -9,6 +10,11 @@ export default async function ({
params: Promise<{ websiteId: string }>;
}) {
const { websiteId } = await params;
+ const website = await getWebsite(websiteId);
+
+ if (!website || website?.deletedAt) {
+ return null;
+ }
return {children};
}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
index 10763618..9cbbd371 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
@@ -74,8 +74,9 @@ export function RealtimeLog({ data }: { data: any }) {
os: string;
country: string;
device: string;
+ hostname: string;
}) => {
- const { __type, eventName, urlPath, browser, os, country, device } = log;
+ const { __type, eventName, urlPath, browser, os, country, device, hostname } = log;
if (__type === TYPE_EVENT) {
return (
@@ -86,7 +87,8 @@ export function RealtimeLog({ data }: { data: any }) {
url: (
@@ -100,7 +102,12 @@ export function RealtimeLog({ data }: { data: any }) {
if (__type === TYPE_PAGEVIEW) {
return (
-
+
{urlPath}
);
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
index cbb28108..df0ef834 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
@@ -39,10 +39,23 @@ export function SessionActivity({
const { isMobile } = useMobile();
let lastDay = null;
+ const renderLink = (label: string, hostname: string) => {
+ return (
+
+ {label}
+
+ );
+ };
+
return (
- {data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => {
+ {data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hostname, hasData }) => {
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
lastDay = createdAt;
@@ -61,7 +74,7 @@ export function SessionActivity({
: formatMessage(labels.viewedPage)}
- {eventName || urlPath}
+ {eventName || renderLink(urlPath, hostname)}
{hasData > 0 && }
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx
new file mode 100644
index 00000000..35e96df3
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx
@@ -0,0 +1,57 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function ShareDeleteButton({
+ shareId,
+ slug,
+ onSave,
+}: {
+ shareId: string;
+ slug: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${shareId}`);
+ const { touch } = useModified();
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('shares');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ title={formatMessage(labels.confirm)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+ {slug},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx
new file mode 100644
index 00000000..df1c2e64
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx
@@ -0,0 +1,16 @@
+import { useMessages } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { ShareEditForm } from './ShareEditForm';
+
+export function ShareEditButton({ shareId }: { shareId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ } title={formatMessage(labels.share)} variant="quiet" width="600px">
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
new file mode 100644
index 00000000..4b86247a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
@@ -0,0 +1,164 @@
+import {
+ Button,
+ Checkbox,
+ Column,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Grid,
+ Label,
+ Loading,
+ Row,
+ Text,
+ TextField,
+} from '@umami/react-zen';
+import { useEffect, useState } from 'react';
+import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
+import { SHARE_NAV_ITEMS } from './constants';
+
+export function ShareEditForm({
+ shareId,
+ websiteId,
+ onSave,
+ onClose,
+}: {
+ shareId?: string;
+ websiteId?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+ const { cloudMode } = useConfig();
+ const { get, post } = useApi();
+ const { touch } = useModified();
+ const { modified } = useModified('shares');
+ const [share, setShare] = useState(null);
+ const [isLoading, setIsLoading] = useState(!!shareId);
+ const [isPending, setIsPending] = useState(false);
+ const [error, setError] = useState(null);
+
+ const isEditing = !!shareId;
+
+ const getUrl = (slug: string) => {
+ if (cloudMode) {
+ return `${process.env.cloudUrl}/share/${slug}`;
+ }
+ return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
+ };
+
+ useEffect(() => {
+ if (!shareId) return;
+
+ const loadShare = async () => {
+ setIsLoading(true);
+ try {
+ const data = await get(`/share/id/${shareId}`);
+ setShare(data);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadShare();
+ }, [shareId, modified]);
+
+ const handleSubmit = async (data: any) => {
+ const parameters: Record = {};
+ SHARE_NAV_ITEMS.forEach(section => {
+ section.items.forEach(item => {
+ parameters[item.id] = data[item.id] ?? false;
+ });
+ });
+
+ setIsPending(true);
+ setError(null);
+
+ try {
+ if (isEditing) {
+ await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
+ } else {
+ await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
+ }
+ touch('shares');
+ onSave?.();
+ onClose?.();
+ } catch (e) {
+ setError(e);
+ } finally {
+ setIsPending(false);
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ const url = isEditing ? getUrl(share?.slug || '') : null;
+
+ // Build default values from share parameters
+ const defaultValues: Record = {
+ name: share?.name || '',
+ };
+ SHARE_NAV_ITEMS.forEach(section => {
+ section.items.forEach(item => {
+ const defaultSelected = item.id === 'overview' || item.id === 'events';
+ defaultValues[item.id] = share?.parameters?.[item.id] ?? defaultSelected;
+ });
+ });
+
+ // Get all item ids for validation
+ const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
new file mode 100644
index 00000000..57701ac6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
@@ -0,0 +1,46 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import { DateDistance } from '@/components/common/DateDistance';
+import { ExternalLink } from '@/components/common/ExternalLink';
+import { useConfig, useMessages } from '@/components/hooks';
+import { ShareDeleteButton } from './ShareDeleteButton';
+import { ShareEditButton } from './ShareEditButton';
+
+export function SharesTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { cloudMode } = useConfig();
+
+ const getUrl = (slug: string) => {
+ return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
+ };
+
+ return (
+
+
+ {({ name }: any) => name}
+
+
+ {({ slug }: any) => {
+ const url = getUrl(slug);
+ return (
+
+ {url}
+
+ );
+ }}
+
+
+ {(row: any) => }
+
+
+ {({ id, slug }: any) => {
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
index 3970cdbd..d39c4531 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
@@ -1,14 +1,11 @@
import { Column } from '@umami/react-zen';
import { Panel } from '@/components/common/Panel';
-import { useWebsite } from '@/components/hooks';
import { WebsiteData } from './WebsiteData';
import { WebsiteEditForm } from './WebsiteEditForm';
import { WebsiteShareForm } from './WebsiteShareForm';
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
- const website = useWebsite();
-
return (
@@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
-
+
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
index 56c6f436..8472ca97 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
@@ -1,93 +1,43 @@
-import {
- Button,
- Column,
- Form,
- FormButtons,
- FormSubmitButton,
- IconLabel,
- Label,
- Row,
- Switch,
- TextField,
-} from '@umami/react-zen';
-import { RefreshCcw } from 'lucide-react';
-import { useState } from 'react';
-import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks';
-import { getRandomChars } from '@/lib/generate';
-
-const generateId = () => getRandomChars(16);
+import { Column, Heading, Row, Text } from '@umami/react-zen';
+import { Plus } from 'lucide-react';
+import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
+import { DialogButton } from '@/components/input/DialogButton';
+import { ShareEditForm } from './ShareEditForm';
+import { SharesTable } from './SharesTable';
export interface WebsiteShareFormProps {
websiteId: string;
- shareId?: string;
- onSave?: () => void;
- onClose?: () => void;
}
-export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
- const { formatMessage, labels, messages, getErrorMessage } = useMessages();
- const [currentId, setCurrentId] = useState(shareId);
- const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
- const { cloudMode } = useConfig();
+export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
- const getUrl = (shareId: string) => {
- if (cloudMode) {
- return `${process.env.cloudUrl}/share/${shareId}`;
- }
-
- return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
- };
-
- const url = getUrl(currentId);
-
- const handleGenerate = () => {
- setCurrentId(generateId());
- };
-
- const handleSwitch = () => {
- setCurrentId(currentId ? null : generateId());
- };
-
- const handleSave = async () => {
- const data = {
- shareId: currentId,
- };
- await mutateAsync(data, {
- onSuccess: async () => {
- toast(formatMessage(messages.saved));
- touch(`website:${websiteId}`);
- onSave?.();
- onClose?.();
- },
- });
- };
+ const shares = data?.data || [];
+ const hasShares = shares.length > 0;
return (
-
+
+
+ {formatMessage(labels.share)}
+ }
+ label={formatMessage(labels.add)}
+ title={formatMessage(labels.share)}
+ variant="primary"
+ width="600px"
+ >
+ {({ close }) => }
+
+
+ {hasShares ? (
+ <>
+ {formatMessage(messages.shareUrl)}
+
+ >
+ ) : (
+ {formatMessage(messages.noDataAvailable)}
+ )}
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/settings/constants.ts b/src/app/(main)/websites/[websiteId]/settings/constants.ts
new file mode 100644
index 00000000..f4a3df80
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/constants.ts
@@ -0,0 +1,30 @@
+export const SHARE_NAV_ITEMS = [
+ {
+ section: 'traffic',
+ items: [
+ { id: 'overview', label: 'overview' },
+ { id: 'events', label: 'events' },
+ { id: 'sessions', label: 'sessions' },
+ { id: 'realtime', label: 'realtime' },
+ { id: 'compare', label: 'compare' },
+ { id: 'breakdown', label: 'breakdown' },
+ ],
+ },
+ {
+ section: 'behavior',
+ items: [
+ { id: 'goals', label: 'goals' },
+ { id: 'funnels', label: 'funnels' },
+ { id: 'journeys', label: 'journeys' },
+ { id: 'retention', label: 'retention' },
+ ],
+ },
+ {
+ section: 'growth',
+ items: [
+ { id: 'utm', label: 'utm' },
+ { id: 'revenue', label: 'revenue' },
+ { id: 'attribution', label: 'attribution' },
+ ],
+ },
+];
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
index 7bf0a813..153f1f52 100644
--- a/src/app/api/auth/logout/route.ts
+++ b/src/app/api/auth/logout/route.ts
@@ -1,7 +1,14 @@
import redis from '@/lib/redis';
+import { parseRequest } from '@/lib/request';
import { ok } from '@/lib/response';
export async function POST(request: Request) {
+ const { error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
if (redis.enabled) {
const token = request.headers.get('authorization')?.split(' ')?.[1];
diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts
index bba3dde3..f8222869 100644
--- a/src/app/api/auth/sso/route.ts
+++ b/src/app/api/auth/sso/route.ts
@@ -1,7 +1,7 @@
import { saveAuth } from '@/lib/auth';
import redis from '@/lib/redis';
import { parseRequest } from '@/lib/request';
-import { json } from '@/lib/response';
+import { json, serverError } from '@/lib/response';
export async function POST(request: Request) {
const { auth, error } = await parseRequest(request);
@@ -10,9 +10,13 @@ export async function POST(request: Request) {
return error();
}
- if (redis.enabled) {
- const token = await saveAuth({ userId: auth.user.id }, 86400);
-
- return json({ user: auth.user, token });
+ if (!redis.enabled) {
+ return serverError({
+ message: 'Redis is disabled',
+ });
}
+
+ const token = await saveAuth({ userId: auth.user.id }, 86400);
+
+ return json({ user: auth.user, token });
}
diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts
index 4e40caa4..101a1224 100644
--- a/src/app/api/config/route.ts
+++ b/src/app/api/config/route.ts
@@ -17,5 +17,6 @@ export async function GET(request: Request) {
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
updatesDisabled: !!process.env.DISABLE_UPDATES,
+ currentVersion: !!process.env.currentVersion,
});
}
diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts
index 29e85319..b53d225d 100644
--- a/src/app/api/reports/journey/route.ts
+++ b/src/app/api/reports/journey/route.ts
@@ -12,11 +12,16 @@ export async function POST(request: Request) {
}
const { websiteId, parameters, filters } = body;
+ const { eventType } = parameters;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
+ if (eventType) {
+ filters.eventType = eventType;
+ }
+
const queryFilters = await getQueryFilters(filters, websiteId);
const data = await getJourney(websiteId, parameters, queryFilters);
diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts
deleted file mode 100644
index bef87c4f..00000000
--- a/src/app/api/share/[shareId]/route.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { secret } from '@/lib/crypto';
-import { createToken } from '@/lib/jwt';
-import { json, notFound } from '@/lib/response';
-import { getSharedWebsite } from '@/queries/prisma';
-
-export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
- const { shareId } = await params;
-
- const website = await getSharedWebsite(shareId);
-
- if (!website) {
- return notFound();
- }
-
- const data = { websiteId: website.id };
- const token = createToken(data, secret());
-
- return json({ ...data, token });
-}
diff --git a/src/app/api/share/[slug]/route.ts b/src/app/api/share/[slug]/route.ts
new file mode 100644
index 00000000..e7d5372f
--- /dev/null
+++ b/src/app/api/share/[slug]/route.ts
@@ -0,0 +1,76 @@
+import { ROLES } from '@/lib/constants';
+import { secret } from '@/lib/crypto';
+import { createToken } from '@/lib/jwt';
+import prisma from '@/lib/prisma';
+import redis from '@/lib/redis';
+import { json, notFound } from '@/lib/response';
+import type { WhiteLabel } from '@/lib/types';
+import { getShareByCode, getWebsite } from '@/queries/prisma';
+
+async function getAccountId(website: { userId?: string; teamId?: string }): Promise {
+ if (website.userId) {
+ return website.userId;
+ }
+
+ if (website.teamId) {
+ const teamOwner = await prisma.client.teamUser.findFirst({
+ where: {
+ teamId: website.teamId,
+ role: ROLES.teamOwner,
+ },
+ select: {
+ userId: true,
+ },
+ });
+
+ return teamOwner?.userId || null;
+ }
+
+ return null;
+}
+
+async function getWhiteLabel(accountId: string): Promise {
+ if (!redis.enabled) {
+ return null;
+ }
+
+ const data = await redis.client.get(`white-label:${accountId}`);
+
+ if (data) {
+ return data as WhiteLabel;
+ }
+
+ return null;
+}
+
+export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
+ const { slug } = await params;
+
+ const share = await getShareByCode(slug);
+
+ if (!share) {
+ return notFound();
+ }
+
+ const website = await getWebsite(share.entityId);
+
+ const data: Record = {
+ shareId: share.id,
+ websiteId: share.entityId,
+ parameters: share.parameters,
+ };
+
+ data.token = createToken(data, secret());
+
+ const accountId = await getAccountId(website);
+
+ if (accountId) {
+ const whiteLabel = await getWhiteLabel(accountId);
+
+ if (whiteLabel) {
+ data.whiteLabel = whiteLabel;
+ }
+ }
+
+ return json(data);
+}
diff --git a/src/app/api/share/id/[shareId]/route.ts b/src/app/api/share/id/[shareId]/route.ts
new file mode 100644
index 00000000..80da17b8
--- /dev/null
+++ b/src/app/api/share/id/[shareId]/route.ts
@@ -0,0 +1,82 @@
+import z from 'zod';
+import { parseRequest } from '@/lib/request';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { anyObjectParam } from '@/lib/schema';
+import { canDeleteEntity, canUpdateEntity, canViewEntity } from '@/permissions';
+import { deleteShare, getShare, updateShare } from '@/queries/prisma';
+
+export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { shareId } = await params;
+
+ const share = await getShare(shareId);
+
+ if (!(await canViewEntity(auth, share.entityId))) {
+ return unauthorized();
+ }
+
+ return json(share);
+}
+
+export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
+ const schema = z.object({
+ name: z.string().max(200),
+ slug: z.string().max(100),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { shareId } = await params;
+ const { name, slug, parameters } = body;
+
+ const share = await getShare(shareId);
+
+ if (!share) {
+ return notFound();
+ }
+
+ if (!(await canUpdateEntity(auth, share.entityId))) {
+ return unauthorized();
+ }
+
+ const result = await updateShare(shareId, {
+ name,
+ slug,
+ parameters,
+ } as any);
+
+ return json(result);
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ shareId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { shareId } = await params;
+
+ const share = await getShare(shareId);
+
+ if (!(await canDeleteEntity(auth, share.entityId))) {
+ return unauthorized();
+ }
+
+ await deleteShare(shareId);
+
+ return ok();
+}
diff --git a/src/app/api/share/route.ts b/src/app/api/share/route.ts
new file mode 100644
index 00000000..a772b4ab
--- /dev/null
+++ b/src/app/api/share/route.ts
@@ -0,0 +1,39 @@
+import z from 'zod';
+import { uuid } from '@/lib/crypto';
+import { getRandomChars } from '@/lib/generate';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { anyObjectParam } from '@/lib/schema';
+import { canUpdateEntity } from '@/permissions';
+import { createShare } from '@/queries/prisma';
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ entityId: z.uuid(),
+ shareType: z.coerce.number().int(),
+ slug: z.string().max(100).optional(),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { entityId, shareType, slug, parameters } = body;
+
+ if (!(await canUpdateEntity(auth, entityId))) {
+ return unauthorized();
+ }
+
+ const share = await createShare({
+ id: uuid(),
+ entityId,
+ shareType,
+ slug: slug || getRandomChars(16),
+ parameters,
+ });
+
+ return json(share);
+}
diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts
index 53ef5923..c571f405 100644
--- a/src/app/api/teams/route.ts
+++ b/src/app/api/teams/route.ts
@@ -28,6 +28,7 @@ export async function GET(request: Request) {
export async function POST(request: Request) {
const schema = z.object({
name: z.string().max(50),
+ ownerId: z.uuid().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -40,7 +41,7 @@ export async function POST(request: Request) {
return unauthorized();
}
- const { name } = body;
+ const { name, ownerId } = body;
const team = await createTeam(
{
@@ -48,7 +49,7 @@ export async function POST(request: Request) {
name,
accessCode: `team_${getRandomChars(16)}`,
},
- auth.user.id,
+ ownerId ?? auth.user.id,
);
return json(team);
diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts
index aade8aa8..e642fe3c 100644
--- a/src/app/api/users/[userId]/route.ts
+++ b/src/app/api/users/[userId]/route.ts
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { hashPassword } from '@/lib/password';
import { parseRequest } from '@/lib/request';
-import { badRequest, json, ok, unauthorized } from '@/lib/response';
+import { badRequest, json, notFound, ok, unauthorized } from '@/lib/response';
import { userRoleParam } from '@/lib/schema';
import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma';
@@ -27,7 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
username: z.string().max(255).optional(),
- password: z.string().max(255).optional(),
+ password: z.string().min(8).max(255).optional(),
role: userRoleParam.optional(),
});
@@ -47,6 +47,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ use
const user = await getUser(userId);
+ if (!user) {
+ return notFound();
+ }
+
const data: any = {};
if (password) {
diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts
index dbb114cf..4335c33f 100644
--- a/src/app/api/users/route.ts
+++ b/src/app/api/users/route.ts
@@ -4,6 +4,7 @@ import { uuid } from '@/lib/crypto';
import { hashPassword } from '@/lib/password';
import { parseRequest } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response';
+import { userRoleParam } from '@/lib/schema';
import { canCreateUser } from '@/permissions';
import { createUser, getUserByUsername } from '@/queries/prisma';
@@ -11,8 +12,8 @@ export async function POST(request: Request) {
const schema = z.object({
id: z.uuid().optional(),
username: z.string().max(255),
- password: z.string(),
- role: z.string().regex(/admin|user|view-only/i),
+ password: z.string().min(8).max(255),
+ role: userRoleParam,
});
const { auth, body, error } = await parseRequest(request, schema);
diff --git a/src/app/api/websites/[websiteId]/events/stats/route.ts b/src/app/api/websites/[websiteId]/events/stats/route.ts
new file mode 100644
index 00000000..61e151d4
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/events/stats/route.ts
@@ -0,0 +1,34 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteEventStats } from '@/queries/sql/events/getWebsiteEventStats';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getWebsiteEventStats(websiteId, filters);
+
+ return json({ data });
+}
diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts
index b4c0e7e8..59f314d3 100644
--- a/src/app/api/websites/[websiteId]/route.ts
+++ b/src/app/api/websites/[websiteId]/route.ts
@@ -1,7 +1,6 @@
import { z } from 'zod';
-import { SHARE_ID_REGEX } from '@/lib/constants';
import { parseRequest } from '@/lib/request';
-import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
+import { json, ok, unauthorized } from '@/lib/response';
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
@@ -33,7 +32,6 @@ export async function POST(
const schema = z.object({
name: z.string().optional(),
domain: z.string().optional(),
- shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -43,23 +41,15 @@ export async function POST(
}
const { websiteId } = await params;
- const { name, domain, shareId } = body;
+ const { name, domain } = body;
if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
}
- try {
- const website = await updateWebsite(websiteId, { name, domain, shareId });
+ const website = await updateWebsite(websiteId, { name, domain });
- return Response.json(website);
- } catch (e: any) {
- if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
- return badRequest({ message: 'That share ID is already taken.' });
- }
-
- return serverError(e);
- }
+ return Response.json(website);
}
export async function DELETE(
diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts
index 45927656..db34193e 100644
--- a/src/app/api/websites/[websiteId]/segments/route.ts
+++ b/src/app/api/websites/[websiteId]/segments/route.ts
@@ -2,7 +2,7 @@ import { z } from 'zod';
import { uuid } from '@/lib/crypto';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
-import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema';
+import { searchParams, segmentParamSchema, segmentTypeParam } from '@/lib/schema';
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
import { createSegment, getWebsiteSegments } from '@/queries/prisma';
@@ -42,7 +42,7 @@ export async function POST(
const schema = z.object({
type: segmentTypeParam,
name: z.string().max(200),
- parameters: anyObjectParam,
+ parameters: segmentParamSchema,
});
const { auth, body, error } = await parseRequest(request, schema);
diff --git a/src/app/api/websites/[websiteId]/shares/route.ts b/src/app/api/websites/[websiteId]/shares/route.ts
new file mode 100644
index 00000000..65d53771
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/shares/route.ts
@@ -0,0 +1,77 @@
+import { z } from 'zod';
+import { ENTITY_TYPE } from '@/lib/constants';
+import { uuid } from '@/lib/crypto';
+import { getRandomChars } from '@/lib/generate';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
+import { canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { createShare, getSharesByEntityId } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...filterParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { page, pageSize, search } = query;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getSharesByEntityId(websiteId, {
+ page,
+ pageSize,
+ search,
+ });
+
+ return json(data);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ name: z.string().max(200),
+ parameters: anyObjectParam.optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { name, parameters } = body;
+ const shareParameters = parameters ?? {};
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const slug = getRandomChars(16);
+
+ const share = await createShare({
+ id: uuid(),
+ entityId: websiteId,
+ shareType: ENTITY_TYPE.website,
+ name,
+ slug,
+ parameters: shareParameters,
+ });
+
+ return json(share);
+}
diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts
index 07c8b969..9d21f4f5 100644
--- a/src/app/api/websites/[websiteId]/stats/route.ts
+++ b/src/app/api/websites/[websiteId]/stats/route.ts
@@ -31,7 +31,11 @@ export async function GET(
const data = await getWebsiteStats(websiteId, filters);
- const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
+ const { startDate, endDate } = getCompareDate(
+ filters.compare ?? 'prev',
+ filters.startDate,
+ filters.endDate,
+ );
const comparison = await getWebsiteStats(websiteId, {
...filters,
diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts
index e2b26c10..dd8e0ffd 100644
--- a/src/app/api/websites/route.ts
+++ b/src/app/api/websites/route.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { uuid } from '@/lib/crypto';
-import redis from '@/lib/redis';
+import { fetchAccount } from '@/lib/load';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { pagingParams, searchParams } from '@/lib/schema';
@@ -52,7 +52,7 @@ export async function POST(request: Request) {
const { id, name, domain, shareId, teamId } = body;
if (process.env.CLOUD_MODE && !teamId) {
- const account = await redis.client.get(`account:${auth.user.id}`);
+ const account = await fetchAccount(auth.user.id);
if (!account?.hasSubscription) {
const count = await getWebsiteCount(auth.user.id);
diff --git a/src/app/share/[...shareId]/Footer.tsx b/src/app/share/[...shareId]/Footer.tsx
deleted file mode 100644
index f2948628..00000000
--- a/src/app/share/[...shareId]/Footer.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Row, Text } from '@umami/react-zen';
-import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
-
-export function Footer() {
- return (
-
-
- umami {`v${CURRENT_VERSION}`}
-
-
- );
-}
diff --git a/src/app/share/[...shareId]/Header.tsx b/src/app/share/[...shareId]/Header.tsx
deleted file mode 100644
index d7b7dcb4..00000000
--- a/src/app/share/[...shareId]/Header.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
-import { LanguageButton } from '@/components/input/LanguageButton';
-import { PreferencesButton } from '@/components/input/PreferencesButton';
-import { Logo } from '@/components/svg';
-
-export function Header() {
- return (
-
-
-
-
-
-
- umami
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/share/[...shareId]/ShareFooter.tsx b/src/app/share/[...shareId]/ShareFooter.tsx
new file mode 100644
index 00000000..5348ac63
--- /dev/null
+++ b/src/app/share/[...shareId]/ShareFooter.tsx
@@ -0,0 +1,23 @@
+import { Row, Text } from '@umami/react-zen';
+import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
+import type { WhiteLabel } from '@/lib/types';
+
+export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
+ if (whiteLabel) {
+ return (
+
+
+ {whiteLabel.name}
+
+
+ );
+ }
+
+ return (
+
+
+ umami {`v${CURRENT_VERSION}`}
+
+
+ );
+}
diff --git a/src/app/share/[...shareId]/ShareHeader.tsx b/src/app/share/[...shareId]/ShareHeader.tsx
new file mode 100644
index 00000000..abd8511d
--- /dev/null
+++ b/src/app/share/[...shareId]/ShareHeader.tsx
@@ -0,0 +1,33 @@
+import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
+import { LanguageButton } from '@/components/input/LanguageButton';
+import { PreferencesButton } from '@/components/input/PreferencesButton';
+import { Logo } from '@/components/svg';
+import type { WhiteLabel } from '@/lib/types';
+
+export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
+ const logoUrl = whiteLabel?.url || 'https://umami.is';
+ const logoName = whiteLabel?.name || 'umami';
+ const logoImage = whiteLabel?.image;
+
+ return (
+
+
+
+ {logoImage ? (
+
+ ) : (
+
+
+
+ )}
+ {logoName}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[...shareId]/ShareNav.tsx
new file mode 100644
index 00000000..b494046d
--- /dev/null
+++ b/src/app/share/[...shareId]/ShareNav.tsx
@@ -0,0 +1,143 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { SideMenu } from '@/components/common/SideMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
+import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
+
+export function ShareNav({
+ shareId,
+ parameters,
+ onItemClick,
+}: {
+ shareId: string;
+ parameters: Record;
+ onItemClick?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { pathname } = useNavigation();
+
+ const renderPath = (path: string) => `/share/${shareId}${path}`;
+
+ const allItems = [
+ {
+ section: 'traffic',
+ label: formatMessage(labels.traffic),
+ items: [
+ {
+ id: 'overview',
+ label: formatMessage(labels.overview),
+ icon: ,
+ path: renderPath(''),
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ icon: ,
+ path: renderPath('/events'),
+ },
+ {
+ id: 'sessions',
+ label: formatMessage(labels.sessions),
+ icon: ,
+ path: renderPath('/sessions'),
+ },
+ {
+ id: 'realtime',
+ label: formatMessage(labels.realtime),
+ icon: ,
+ path: renderPath('/realtime'),
+ },
+ {
+ id: 'compare',
+ label: formatMessage(labels.compare),
+ icon: ,
+ path: renderPath('/compare'),
+ },
+ {
+ id: 'breakdown',
+ label: formatMessage(labels.breakdown),
+ icon: ,
+ path: renderPath('/breakdown'),
+ },
+ ],
+ },
+ {
+ section: 'behavior',
+ label: formatMessage(labels.behavior),
+ items: [
+ {
+ id: 'goals',
+ label: formatMessage(labels.goals),
+ icon: ,
+ path: renderPath('/goals'),
+ },
+ {
+ id: 'funnels',
+ label: formatMessage(labels.funnels),
+ icon: ,
+ path: renderPath('/funnels'),
+ },
+ {
+ id: 'journeys',
+ label: formatMessage(labels.journeys),
+ icon: ,
+ path: renderPath('/journeys'),
+ },
+ {
+ id: 'retention',
+ label: formatMessage(labels.retention),
+ icon: ,
+ path: renderPath('/retention'),
+ },
+ ],
+ },
+ {
+ section: 'growth',
+ label: formatMessage(labels.growth),
+ items: [
+ {
+ id: 'utm',
+ label: formatMessage(labels.utm),
+ icon: ,
+ path: renderPath('/utm'),
+ },
+ {
+ id: 'revenue',
+ label: formatMessage(labels.revenue),
+ icon: ,
+ path: renderPath('/revenue'),
+ },
+ {
+ id: 'attribution',
+ label: formatMessage(labels.attribution),
+ icon: ,
+ path: renderPath('/attribution'),
+ },
+ ],
+ },
+ ];
+
+ // Filter items based on parameters
+ const items = allItems
+ .map(section => ({
+ label: section.label,
+ items: section.items.filter(item => parameters[item.id] !== false),
+ }))
+ .filter(section => section.items.length > 0);
+
+ const selectedKey = items
+ .flatMap(e => e.items)
+ .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx
index 7ed06673..91a8b298 100644
--- a/src/app/share/[...shareId]/SharePage.tsx
+++ b/src/app/share/[...shareId]/SharePage.tsx
@@ -1,17 +1,74 @@
'use client';
-import { Column, useTheme } from '@umami/react-zen';
-import { useEffect } from 'react';
+import { Column, Grid, Row, useTheme } from '@umami/react-zen';
+import { useRouter } from 'next/navigation';
+import { useEffect, useMemo } from 'react';
+import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
+import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
+import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
+import { GoalsPage } from '@/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage';
+import { JourneysPage } from '@/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage';
+import { RetentionPage } from '@/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage';
+import { RevenuePage } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage';
+import { UTMPage } from '@/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage';
+import { ComparePage } from '@/app/(main)/websites/[websiteId]/compare/ComparePage';
+import { EventsPage } from '@/app/(main)/websites/[websiteId]/events/EventsPage';
+import { RealtimePage } from '@/app/(main)/websites/[websiteId]/realtime/RealtimePage';
+import { SessionsPage } from '@/app/(main)/websites/[websiteId]/sessions/SessionsPage';
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { PageBody } from '@/components/common/PageBody';
import { useShareTokenQuery } from '@/components/hooks';
-import { Footer } from './Footer';
-import { Header } from './Header';
+import { MobileMenuButton } from '@/components/input/MobileMenuButton';
+import { ShareFooter } from './ShareFooter';
+import { ShareHeader } from './ShareHeader';
+import { ShareNav } from './ShareNav';
-export function SharePage({ shareId }) {
+const PAGE_COMPONENTS: Record> = {
+ '': WebsitePage,
+ overview: WebsitePage,
+ events: EventsPage,
+ sessions: SessionsPage,
+ realtime: RealtimePage,
+ compare: ComparePage,
+ breakdown: BreakdownPage,
+ goals: GoalsPage,
+ funnels: FunnelsPage,
+ journeys: JourneysPage,
+ retention: RetentionPage,
+ utm: UTMPage,
+ revenue: RevenuePage,
+ attribution: AttributionPage,
+};
+
+// All section IDs that can be enabled/disabled via parameters
+const ALL_SECTION_IDS = [
+ 'overview',
+ 'events',
+ 'sessions',
+ 'realtime',
+ 'compare',
+ 'breakdown',
+ 'goals',
+ 'funnels',
+ 'journeys',
+ 'retention',
+ 'utm',
+ 'revenue',
+ 'attribution',
+];
+
+export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
const { shareToken, isLoading } = useShareTokenQuery(shareId);
const { setTheme } = useTheme();
+ const router = useRouter();
+
+ // Calculate allowed sections
+ const allowedSections = useMemo(() => {
+ if (!shareToken?.parameters) return [];
+ const params = shareToken.parameters;
+ return ALL_SECTION_IDS.filter(id => params[id] !== false);
+ }, [shareToken?.parameters]);
useEffect(() => {
const url = new URL(window?.location?.href);
@@ -22,20 +79,77 @@ export function SharePage({ shareId }) {
}
}, []);
+ // Redirect to the only allowed section if there's just one and we're on the base path
+ useEffect(() => {
+ if (
+ allowedSections.length === 1 &&
+ allowedSections[0] !== 'overview' &&
+ (path === '' || path === 'overview')
+ ) {
+ router.replace(`/share/${shareId}/${allowedSections[0]}`);
+ }
+ }, [allowedSections, shareId, path, router]);
+
if (isLoading || !shareToken) {
return null;
}
+ const { websiteId, parameters = {}, whiteLabel } = shareToken;
+
+ // Redirect to only allowed section - return null while redirecting
+ if (
+ allowedSections.length === 1 &&
+ allowedSections[0] !== 'overview' &&
+ (path === '' || path === 'overview')
+ ) {
+ return null;
+ }
+
+ // Check if the requested path is allowed
+ const pageKey = path || '';
+ const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
+
+ if (!isAllowed) {
+ return null;
+ }
+
+ const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
+
return (
-
-
-
-
-
-
-
-
+
+
+
+
+ {({ close }) => {
+ return ;
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx
index b9900eb7..3a21f836 100644
--- a/src/app/share/[...shareId]/page.tsx
+++ b/src/app/share/[...shareId]/page.tsx
@@ -2,6 +2,7 @@ import { SharePage } from './SharePage';
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
const { shareId } = await params;
+ const [slug, ...path] = shareId;
- return ;
+ return ;
}
diff --git a/src/components/common/PageBody.tsx b/src/components/common/PageBody.tsx
index f07e589b..c70b9dfe 100644
--- a/src/components/common/PageBody.tsx
+++ b/src/components/common/PageBody.tsx
@@ -31,6 +31,7 @@ export function PageBody({
({
queryKey: [
'websites:event-data:values',
- { websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
+ { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
],
queryFn: () =>
get(`/websites/${websiteId}/event-data/values`, {
diff --git a/src/components/hooks/queries/useEventStatsQuery.ts b/src/components/hooks/queries/useEventStatsQuery.ts
new file mode 100644
index 00000000..44316ca5
--- /dev/null
+++ b/src/components/hooks/queries/useEventStatsQuery.ts
@@ -0,0 +1,37 @@
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import { useApi } from '../useApi';
+import { useFilterParameters } from '../useFilterParameters';
+
+export interface EventStatsData {
+ events: number;
+ visitors: number;
+ visits: number;
+ uniqueEvents: number;
+}
+
+type EventStatsApiResponse = {
+ data: EventStatsData;
+};
+
+export function useEventStatsQuery(
+ { websiteId }: { websiteId: string },
+ options?: UseQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['websites:events:stats', { websiteId, startAt, endAt, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/events/stats`, {
+ startAt,
+ endAt,
+ ...filters,
+ }),
+ select: response => response.data,
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts
index c6fce128..1828e594 100644
--- a/src/components/hooks/queries/useResultQuery.ts
+++ b/src/components/hooks/queries/useResultQuery.ts
@@ -10,7 +10,7 @@ export function useResultQuery(
) {
const { websiteId, ...parameters } = params;
const { post, useQuery } = useApi();
- const { startDate, endDate, timezone } = useDateParameters();
+ const { startDate, endDate, timezone, unit } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
@@ -22,6 +22,7 @@ export function useResultQuery(
startDate,
endDate,
timezone,
+ unit,
...params,
...filters,
},
@@ -35,6 +36,7 @@ export function useResultQuery(
startDate,
endDate,
timezone,
+ unit,
...parameters,
},
}),
diff --git a/src/components/hooks/queries/useShareTokenQuery.ts b/src/components/hooks/queries/useShareTokenQuery.ts
index dbad3dcd..446e33da 100644
--- a/src/components/hooks/queries/useShareTokenQuery.ts
+++ b/src/components/hooks/queries/useShareTokenQuery.ts
@@ -3,7 +3,7 @@ import { useApi } from '../useApi';
const selector = (state: { shareToken: string }) => state.shareToken;
-export function useShareTokenQuery(shareId: string): {
+export function useShareTokenQuery(slug: string): {
shareToken: any;
isLoading?: boolean;
error?: Error;
@@ -11,9 +11,9 @@ export function useShareTokenQuery(shareId: string): {
const shareToken = useApp(selector);
const { get, useQuery } = useApi();
const { isLoading, error } = useQuery({
- queryKey: ['share', shareId],
+ queryKey: ['share', slug],
queryFn: async () => {
- const data = await get(`/share/${shareId}`);
+ const data = await get(`/share/${slug}`);
setShareToken(data);
diff --git a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
index b2e90199..1611c7f8 100644
--- a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
@@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
options?: ReactQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
@@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
websiteId,
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
},
@@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
get(`/websites/${websiteId}/metrics/expanded`, {
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
}),
diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
index 67c5e4d4..cd064af6 100644
--- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
@@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
options?: ReactQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
@@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
websiteId,
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
},
@@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
get(`/websites/${websiteId}/metrics`, {
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
}),
diff --git a/src/components/hooks/queries/useWebsiteSharesQuery.ts b/src/components/hooks/queries/useWebsiteSharesQuery.ts
new file mode 100644
index 00000000..298e4d26
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSharesQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useWebsiteSharesQuery(
+ { websiteId }: { websiteId: string },
+ options?: ReactQueryOptions,
+) {
+ const { modified } = useModified('shares');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['websiteShares', { websiteId, modified }],
+ queryFn: pageParams => {
+ return get(`/websites/${websiteId}/shares`, pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts
index e9a0c48c..48484a07 100644
--- a/src/components/hooks/queries/useWebsiteStatsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts
@@ -19,17 +19,16 @@ export interface WebsiteStatsData {
}
export function useWebsiteStatsQuery(
- websiteId: string,
+ { websiteId, compare }: { websiteId: string; compare?: string },
options?: UseQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
- queryKey: ['websites:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
- queryFn: () =>
- get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, ...filters }),
+ queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
+ queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
enabled: !!websiteId,
...options,
});
diff --git a/src/components/hooks/queries/useWeeklyTrafficQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
index a76ebb3d..df729ffd 100644
--- a/src/components/hooks/queries/useWeeklyTrafficQuery.ts
+++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
@@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record {
return get(`/websites/${websiteId}/sessions/weekly`, {
startAt,
endAt,
- unit,
timezone,
...params,
...filters,
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts
index 755f36ee..5090bd3d 100644
--- a/src/components/hooks/useDateRange.ts
+++ b/src/components/hooks/useDateRange.ts
@@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
const {
- query: { date = '', offset = 0, compare = 'prev' },
+ query: { date = '', unit = '', offset = 0, compare = 'prev' },
} = useNavigation();
const { locale } = useLocale();
-
const dateRange = useMemo(() => {
const dateRangeObject = parseDateRange(
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
+ unit,
locale,
options.timezone,
);
@@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
return !options.ignoreOffset && offset
? getOffsetDateRange(dateRangeObject, +offset)
: dateRangeObject;
- }, [date, offset, options]);
+ }, [date, unit, offset, options]);
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
return {
date,
+ unit,
offset,
compare,
isAllTime: date.endsWith(`:all`),
diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts
index 22a1dcf3..039b7157 100644
--- a/src/components/hooks/useFields.ts
+++ b/src/components/hooks/useFields.ts
@@ -15,6 +15,7 @@ export function useFields() {
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
+ { name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) },
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
];
diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts
index 54032120..c141a3be 100644
--- a/src/components/hooks/useFilterParameters.ts
+++ b/src/components/hooks/useFilterParameters.ts
@@ -18,6 +18,7 @@ export function useFilterParameters() {
event,
tag,
hostname,
+ distinctId,
page,
pageSize,
search,
@@ -42,6 +43,7 @@ export function useFilterParameters() {
event,
tag,
hostname,
+ distinctId,
search,
segment,
cohort,
@@ -61,6 +63,7 @@ export function useFilterParameters() {
event,
tag,
hostname,
+ distinctId,
page,
pageSize,
search,
diff --git a/src/components/input/FilterEditForm.tsx b/src/components/input/FilterEditForm.tsx
index 44f43844..87acc515 100644
--- a/src/components/input/FilterEditForm.tsx
+++ b/src/components/input/FilterEditForm.tsx
@@ -22,6 +22,7 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
const [currentCohort, setCurrentCohort] = useState(cohort);
const { isMobile } = useMobile();
const excludeFilters = pathname.includes('/pixels') || pathname.includes('/links');
+ const excludeEvent = !pathname.endsWith('/events');
const handleReset = () => {
setCurrentFilters([]);
@@ -61,7 +62,13 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
websiteId={websiteId}
value={currentFilters}
onChange={setCurrentFilters}
- exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
+ exclude={
+ excludeFilters
+ ? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event']
+ : excludeEvent
+ ? ['event']
+ : []
+ }
/>
diff --git a/src/components/input/UnitFilter.tsx b/src/components/input/UnitFilter.tsx
new file mode 100644
index 00000000..84a15f35
--- /dev/null
+++ b/src/components/input/UnitFilter.tsx
@@ -0,0 +1,71 @@
+import { ListItem, Row, Select } from '@umami/react-zen';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getItem } from '@/lib/storage';
+
+export function UnitFilter() {
+ const { formatMessage, labels } = useMessages();
+ const { router, query, updateParams } = useNavigation();
+
+ const DATE_RANGE_UNIT_CONFIG = {
+ '0week': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '7day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '0month': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '30day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '90day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'month'],
+ },
+ '6month': {
+ defaultUnit: 'month',
+ availableUnits: ['month', 'day'],
+ },
+ };
+
+ const unitConfig =
+ DATE_RANGE_UNIT_CONFIG[query.date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE];
+
+ if (!unitConfig) {
+ return null;
+ }
+
+ const handleChange = (value: string) => {
+ router.push(updateParams({ unit: value }));
+ };
+
+ const options = unitConfig.availableUnits.map(unit => ({
+ id: unit,
+ label: formatMessage(labels[unit]),
+ }));
+
+ const selectedUnit = query.unit ?? unitConfig.defaultUnit;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/input/UserSelect.tsx b/src/components/input/UserSelect.tsx
new file mode 100644
index 00000000..ccb3d432
--- /dev/null
+++ b/src/components/input/UserSelect.tsx
@@ -0,0 +1,71 @@
+import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
+import { useMemo, useState } from 'react';
+import { Empty } from '@/components/common/Empty';
+import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks';
+
+export function UserSelect({
+ teamId,
+ onChange,
+ ...props
+}: {
+ teamId?: string;
+} & SelectProps) {
+ const { formatMessage, messages } = useMessages();
+ const { data: users, isLoading: usersLoading } = useUsersQuery();
+ const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId);
+ const [username, setUsername] = useState();
+ const [search, setSearch] = useState('');
+
+ const listItems = useMemo(() => {
+ if (!users) {
+ return [];
+ }
+ if (!teamId || !teamMembers) {
+ return users.data;
+ }
+ const teamMemberIds = teamMembers.data.map(({ userId }) => userId);
+ return users.data.filter(({ id }) => !teamMemberIds.includes(id));
+ }, [users, teamMembers, teamId]);
+
+ const handleSearch = (value: string) => {
+ setSearch(value);
+ };
+
+ const handleOpenChange = () => {
+ setSearch('');
+ };
+
+ const handleChange = (id: string) => {
+ setUsername(listItems.find(item => item.id === id)?.username);
+ onChange(id);
+ };
+
+ const renderValue = () => {
+ return (
+
+ {username}
+
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx
index 18b4f13b..a76058ec 100644
--- a/src/components/input/WebsiteDateFilter.tsx
+++ b/src/components/input/WebsiteDateFilter.tsx
@@ -41,7 +41,7 @@ export function WebsiteDateFilter({
}),
);
} else {
- router.push(updateParams({ date, offset: undefined }));
+ router.push(updateParams({ date, offset: undefined, unit: undefined }));
}
};
diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx
index 8d81eb9a..330f826a 100644
--- a/src/components/input/WebsiteSelect.tsx
+++ b/src/components/input/WebsiteSelect.tsx
@@ -26,7 +26,7 @@ export function WebsiteSelect({
const { user } = useLoginQuery();
const { data, isLoading } = useUserWebsitesQuery(
{ userId: user?.id, teamId },
- { search, pageSize: 10, includeTeams },
+ { search, pageSize: 20, includeTeams },
);
const listItems: { id: string; name: string }[] = data?.data || [];
@@ -65,7 +65,7 @@ export function WebsiteSelect({
renderValue={renderValue}
listProps={{
renderEmptyState: () => ,
- style: { maxHeight: '400px' },
+ style: { maxHeight: 'calc(42vh - 65px)' },
}}
>
{({ id, name }: any) => {name}}
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 0438c06e..de29c306 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -146,6 +146,7 @@ export const labels = defineMessages({
poweredBy: { id: 'label.powered-by', defaultMessage: 'Powered by {name}' },
pageViews: { id: 'label.page-views', defaultMessage: 'Page views' },
uniqueVisitors: { id: 'label.unique-visitors', defaultMessage: 'Unique visitors' },
+ uniqueEvents: { id: 'label.unique-events', defaultMessage: 'Unique Events' },
bounceRate: { id: 'label.bounce-rate', defaultMessage: 'Bounce rate' },
viewsPerVisit: { id: 'label.views-per-visit', defaultMessage: 'Views per visit' },
visitDuration: { id: 'label.visit-duration', defaultMessage: 'Visit duration' },
@@ -245,7 +246,10 @@ export const labels = defineMessages({
tag: { id: 'label.tag', defaultMessage: 'Tag' },
segment: { id: 'label.segment', defaultMessage: 'Segment' },
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
+ minute: { id: 'label.minute', defaultMessage: 'Minute' },
+ hour: { id: 'label.hour', defaultMessage: 'Hour' },
day: { id: 'label.day', defaultMessage: 'Day' },
+ month: { id: 'label.month', defaultMessage: 'Month' },
date: { id: 'label.date', defaultMessage: 'Date' },
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
create: { id: 'label.create', defaultMessage: 'Create' },
@@ -351,6 +355,7 @@ export const labels = defineMessages({
growth: { id: 'label.growth', defaultMessage: 'Growth' },
account: { id: 'label.account', defaultMessage: 'Account' },
application: { id: 'label.application', defaultMessage: 'Application' },
+ version: { id: 'label.version', defaultMessage: 'Version' },
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx
index d15bcf13..590fd5ac 100644
--- a/src/components/metrics/MetricCard.tsx
+++ b/src/components/metrics/MetricCard.tsx
@@ -25,7 +25,7 @@ export const MetricCard = ({
showChange = false,
}: MetricCardProps) => {
const diff = value - change;
- const pct = ((value - diff) / diff) * 100;
+ const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
diff --git a/src/index.ts b/src/index.ts
index 907c5623..df164b9d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,11 +19,13 @@ export * from '@/app/(main)/teams/TeamAddForm';
export * from '@/app/(main)/teams/TeamJoinForm';
export * from '@/app/(main)/teams/TeamLeaveButton';
export * from '@/app/(main)/teams/TeamLeaveForm';
+export * from '@/app/(main)/teams/TeamMemberAddForm';
export * from '@/app/(main)/teams/TeamProvider';
export * from '@/app/(main)/teams/TeamsAddButton';
export * from '@/app/(main)/teams/TeamsDataTable';
export * from '@/app/(main)/teams/TeamsHeader';
export * from '@/app/(main)/teams/TeamsJoinButton';
+export * from '@/app/(main)/teams/TeamsMemberAddButton';
export * from '@/app/(main)/teams/TeamsTable';
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData';
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
diff --git a/src/lang/bg-BG.json b/src/lang/bg-BG.json
index 4b0effc8..50099032 100644
--- a/src/lang/bg-BG.json
+++ b/src/lang/bg-BG.json
@@ -93,7 +93,7 @@
"label.event-name": "Име на събитие",
"label.events": "Събития",
"label.exists": "Съществува",
- "label.exit": "Exit URL",
+ "label.exit": "URL за изход",
"label.false": "Грешно",
"label.field": "Поле",
"label.fields": "Полета",
@@ -135,7 +135,7 @@
"label.last-days": "Последните {x} дни",
"label.last-hours": "Последните {x} часа",
"label.last-months": "Последните {x} месеца",
- "label.last-seen": "Last seen",
+ "label.last-seen": "Последно видяно",
"label.leave": "Напусни",
"label.leave-team": "Напусни екип",
"label.less-than": "По-малко от",
@@ -161,7 +161,7 @@
"label.none": "Няма",
"label.number-of-records": "{x} {x, plural, one {един} other {други}}",
"label.ok": "Добре",
- "label.online": "Online",
+ "label.online": "Онлайн",
"label.organic-search": "Органично търсене",
"label.organic-shopping": "Органично пазаруване",
"label.organic-social": "Органични социални мрежи",
@@ -185,9 +185,9 @@
"label.paths": "Пътища",
"label.pixels": "Пиксели",
"label.powered-by": "Поддържано от {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Предишен",
+ "label.previous-period": "Предишен период",
+ "label.previous-year": "Предишна година",
"label.profile": "Профил",
"label.properties": "Свойства",
"label.property": "Свойство",
@@ -211,8 +211,8 @@
"label.reset-website": "Нулирай уебсайт",
"label.retention": "Привързване",
"label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
+ "label.revenue": "Приходи",
+ "label.revenue-description": "Прегледайте приходите си във времето.",
"label.role": "Роля",
"label.run-query": "Изпълни запитване",
"label.save": "Запази",
@@ -260,14 +260,14 @@
"label.total": "Общо",
"label.total-records": "Общо записи",
"label.tracking-code": "Код за проследяване",
- "label.transactions": "Transactions",
+ "label.transactions": "Транзакции",
"label.transfer": "Прехвърли",
"label.transfer-website": "Прехвърляне на уебсайт",
"label.true": "Вярно",
"label.type": "Вид",
"label.unique": "Уникален",
"label.unique-visitors": "Уникални посетители",
- "label.uniqueCustomers": "Unique Customers",
+ "label.uniqueCustomers": "Уникални клиенти",
"label.unknown": "Неизвестен",
"label.untitled": "Без заглавие",
"label.update": "Актуализирай",
@@ -282,7 +282,7 @@
"label.view-only": "Само за преглед",
"label.views": "Прегледи",
"label.views-per-visit": "Прегледи на посещение",
- "label.visit-duration": "Visit duration",
+ "label.visit-duration": "Продължителност на посещение",
"label.visitors": "Посетители",
"label.visits": "Посещения",
"label.website": "Уебсайт",
@@ -292,8 +292,8 @@
"label.yesterday": "Вчера",
"message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.active-users": "{x} {x, plural, one {активен един} other {активни други}}",
- "message.bad-request": "Bad request",
- "message.collected-data": "Collected data",
+ "message.bad-request": "Невалидна заявка",
+ "message.collected-data": "Събрани данни",
"message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?",
"message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?",
"message.confirm-remove": "Сигурни ли сте, че искате да премахнете {target}?",
@@ -302,7 +302,7 @@
"message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
"message.error": "Възникна грешка.",
"message.event-log": "{event} на {url}",
- "message.forbidden": "Forbidden",
+ "message.forbidden": "Забранено",
"message.go-to-settings": "Отидете в настройките",
"message.incorrect-username-password": "Неправилно потребителско име и/или парола.",
"message.invalid-domain": "Невалиден домейн. Не включвайте http/https.",
@@ -316,13 +316,13 @@
"message.no-teams": "Няма създадени екипи.",
"message.no-users": "Няма потребители.",
"message.no-websites-configured": "Нямате конфигурирани уебсайтове.",
- "message.not-found": "Not found",
- "message.nothing-selected": "Nothing selected.",
+ "message.not-found": "Не е намерено",
+ "message.nothing-selected": "Няма избрано.",
"message.page-not-found": "Страницата не е намерена",
"message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.",
"message.saved": "Запазено.",
- "message.sever-error": "Server error",
+ "message.sever-error": "Сървърна грешка",
"message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:",
"message.team-already-member": "Вече сте член на екипа.",
"message.team-not-found": "Екипът не е намерен.",
@@ -332,7 +332,7 @@
"message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.",
"message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.",
"message.triggered-event": "Активирано събитие",
- "message.unauthorized": "Unauthorized",
+ "message.unauthorized": "Неоторизиран достъп",
"message.user-deleted": "Потребителят е изтрит.",
"message.viewed-page": "Страницата е видяна",
"message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"
diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json
index daaa62f0..68629470 100644
--- a/src/lang/fi-FI.json
+++ b/src/lang/fi-FI.json
@@ -37,7 +37,7 @@
"label.compare-dates": "Vertaa päivämääriä",
"label.confirm": "Vahvista",
"label.confirm-password": "Vahvista salasana",
- "label.contains": "Contains",
+ "label.contains": "Sisältää",
"label.content": "Sisältö",
"label.continue": "Jatka",
"label.conversion": "Konversio",
@@ -96,7 +96,7 @@
"label.false": "Epätosi",
"label.field": "Kenttä",
"label.fields": "Kentät",
- "label.filter": "Filter",
+ "label.filter": "Suodatin",
"label.filter-combined": "Yhdistetty",
"label.filter-raw": "Käsittelemätön",
"label.filters": "Suodattimet",
@@ -151,7 +151,7 @@
"label.members": "Jäsenet",
"label.min": "Minimi",
"label.mobile": "Puhelin",
- "label.model": "Model",
+ "label.model": "Malli",
"label.more": "Lisää",
"label.my-account": "Oma tili",
"label.my-websites": "Omat verkkosivut",
@@ -184,9 +184,9 @@
"label.paths": "Polut",
"label.pixels": "Pikselit",
"label.powered-by": "Voimanlähteenä {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Edellinen",
+ "label.previous-period": "Edellinen ajanjakso",
+ "label.previous-year": "Edellinen vuosi",
"label.profile": "Profiili",
"label.properties": "Ominaisuudet",
"label.property": "Ominaisuus",
@@ -195,16 +195,16 @@
"label.query-parameters": "Kyselyn parametrit",
"label.realtime": "Juuri nyt",
"label.referral": "Viittaus",
- "label.referrer": "Referrer",
+ "label.referrer": "Viittaaja",
"label.referrers": "Viittaajat",
"label.refresh": "Päivitä",
- "label.regenerate": "Regenerate",
- "label.region": "Region",
- "label.regions": "Regions",
+ "label.regenerate": "Luo uudelleen",
+ "label.region": "Alue",
+ "label.regions": "Alueet",
"label.remaining": "Jäljellä",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
+ "label.remove": "Poista",
+ "label.remove-member": "Poista jäsen",
+ "label.reports": "Raportit",
"label.required": "Vaaditaan",
"label.reset": "Nollaa",
"label.reset-website": "Nollaa tilastot",
@@ -212,19 +212,19 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Tulot",
"label.revenue-description": "Katso tulosi ajan mittaan.",
- "label.role": "Role",
- "label.run-query": "Run query",
+ "label.role": "Rooli",
+ "label.run-query": "Suorita kysely",
"label.save": "Tallenna",
"label.screens": "Näytöt",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
+ "label.search": "Haku",
+ "label.select": "Valitse",
+ "label.select-date": "Valitse päivämäärä",
"label.select-filter": "Valitse suodatin",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
+ "label.select-role": "Valitse rooli",
+ "label.select-website": "Valitse verkkosivu",
"label.session": "Istunto",
"label.session-data": "Istuntotiedot",
- "label.sessions": "Sessions",
+ "label.sessions": "Istunnot",
"label.settings": "Asetukset",
"label.share": "Jaa",
"label.share-url": "Jaa URL",
@@ -233,107 +233,107 @@
"label.sources": "Lähteet",
"label.start-step": "Aloitusvaihe",
"label.steps": "Vaiheet",
- "label.sum": "Sum",
+ "label.sum": "Summa",
"label.tablet": "Tabletti",
"label.tag": "Tunniste",
"label.tags": "Tunnisteet",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
+ "label.team": "Tiimi",
+ "label.team-id": "Tiimin ID",
+ "label.team-manager": "Tiimin johtaja",
+ "label.team-member": "Tiimin jäsen",
+ "label.team-name": "Tiimin nimi",
+ "label.team-owner": "Tiimin omistaja",
"label.team-settings": "Tiimin asetukset",
"label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
+ "label.team-websites": "Tiimin verkkosivut",
+ "label.teams": "Tiimit",
"label.terms": "Ehdot",
"label.theme": "Teema",
"label.this-month": "Tämä kuukausi",
"label.this-week": "Tämä viikko",
"label.this-year": "Tämä vuosi",
"label.timezone": "Aikavyöhyke",
- "label.title": "Title",
+ "label.title": "Otsikko",
"label.today": "Tänään",
"label.toggle-charts": "Kytke kaaviot päälle/pois",
- "label.total": "Total",
- "label.total-records": "Total records",
+ "label.total": "Yhteensä",
+ "label.total-records": "Tietueita yhteensä",
"label.tracking-code": "Seurantakoodi",
- "label.transactions": "Transactions",
- "label.transfer": "Transfer",
- "label.transfer-website": "Transfer website",
- "label.true": "True",
- "label.type": "Type",
- "label.unique": "Unique",
+ "label.transactions": "Transaktiot",
+ "label.transfer": "Siirrä",
+ "label.transfer-website": "Siirrä verkkosivu",
+ "label.true": "Tosi",
+ "label.type": "Tyyppi",
+ "label.unique": "Uniikki",
"label.unique-visitors": "Yksittäiset kävijät",
- "label.uniqueCustomers": "Unique Customers",
+ "label.uniqueCustomers": "Uniikit asiakkaat",
"label.unknown": "Tuntematon",
- "label.untitled": "Untitled",
- "label.update": "Update",
- "label.user": "User",
+ "label.untitled": "Nimetön",
+ "label.update": "Päivitä",
+ "label.user": "Käyttäjä",
"label.username": "Käyttäjänimi",
- "label.users": "Users",
+ "label.users": "Käyttäjät",
"label.utm": "UTM",
- "label.utm-description": "Track your campaigns through UTM parameters.",
- "label.value": "Value",
- "label.view": "View",
+ "label.utm-description": "Seuraa kampanjoitasi UTM-parametrien avulla.",
+ "label.value": "Arvo",
+ "label.view": "Näytä",
"label.view-details": "Katso tiedot",
- "label.view-only": "View only",
+ "label.view-only": "Vain katselu",
"label.views": "Näyttökerrat",
- "label.views-per-visit": "Views per visit",
+ "label.views-per-visit": "Katselukerrat vierailua kohti",
"label.visit-duration": "Keskimääräinen vierailuaika",
"label.visitors": "Vierailijat",
- "label.visits": "Visits",
- "label.website": "Website",
- "label.website-id": "Website ID",
+ "label.visits": "Vierailut",
+ "label.website": "Verkkosivu",
+ "label.website-id": "Verkkosivun ID",
"label.websites": "Verkkosivut",
- "label.window": "Window",
- "label.yesterday": "Yesterday",
- "label.behavior": "Behavior",
- "message.action-confirmation": "Type {confirmation} in the box below to confirm.",
+ "label.window": "Ikkuna",
+ "label.yesterday": "Eilen",
+ "label.behavior": "Käyttäytyminen",
+ "message.action-confirmation": "Kirjoita {confirmation} alla olevaan kenttään vahvistaaksesi.",
"message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}",
- "message.bad-request": "Bad request",
- "message.collected-data": "Collected data",
+ "message.bad-request": "Virheellinen pyyntö",
+ "message.collected-data": "Kerätty data",
"message.confirm-delete": "Haluatko varmasti poistaa sivuston {target}?",
- "message.confirm-leave": "Are you sure you want to leave {target}?",
- "message.confirm-remove": "Are you sure you want to remove {target}?",
+ "message.confirm-leave": "Haluatko varmasti poistua {target}?",
+ "message.confirm-remove": "Haluatko varmasti poistaa {target}?",
"message.confirm-reset": "Haluatko varmasti poistaa sivuston {target} tilastot?",
- "message.delete-team-warning": "Deleting a team will also delete all team websites.",
+ "message.delete-team-warning": "Tiimin poistaminen poistaa myös kaikki tiimin sivustot.",
"message.delete-website-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
"message.error": "Jotain meni pieleen.",
"message.event-log": "{event} on {url}",
- "message.forbidden": "Forbidden",
+ "message.forbidden": "Kielletty",
"message.go-to-settings": "Mene asetuksiin",
"message.incorrect-username-password": "Väärä käyttäjänimi/salasana.",
"message.invalid-domain": "Virheellinen verkkotunnus",
- "message.min-password-length": "Minimum length of {n} characters",
- "message.new-version-available": "A new version of Umami {version} is available!",
+ "message.min-password-length": "Vähimmäispituus {n} merkkiä",
+ "message.new-version-available": "Umamista on saatavilla uusi versio {version}!",
"message.no-data-available": "Tietoja ei ole käytettävissä.",
- "message.no-event-data": "No event data is available.",
+ "message.no-event-data": "Tapahtumatietoja ei ole saatavilla.",
"message.no-match-password": "Salasanat eivät täsmää",
- "message.no-results-found": "No results were found.",
- "message.no-team-websites": "This team does not have any websites.",
- "message.no-teams": "You have not created any teams.",
- "message.no-users": "There are no users.",
+ "message.no-results-found": "Tuloksia ei löytynyt.",
+ "message.no-team-websites": "Tällä tiimillä ei ole verkkosivustoja.",
+ "message.no-teams": "Et ole luonut yhtään tiimiä.",
+ "message.no-users": "Käyttäjiä ei ole.",
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",
- "message.not-found": "Not found",
- "message.nothing-selected": "Nothing selected.",
+ "message.not-found": "Ei löytynyt",
+ "message.nothing-selected": "Mitään ei ole valittu.",
"message.page-not-found": "Sivua ei löydetty.",
- "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
+ "message.reset-website": "Nollaa verkkosivusto kirjoittamalla {confirmation} alla olevaan kenttään vahvistaaksesi.",
"message.reset-website-warning": "Kaikki sivuston tilastot poistetaan, mutta seurantakoodi pysyy muuttumattomana.",
"message.saved": "Tallennettu onnistuneesti.",
- "message.sever-error": "Server error",
+ "message.sever-error": "Palvelinvirhe",
"message.share-url": "Tämä on julkisesti jaettu URL sivustolle {target}.",
- "message.team-already-member": "You are already a member of the team.",
- "message.team-not-found": "Team not found.",
- "message.team-websites-info": "Websites can be viewed by anyone on the team.",
+ "message.team-already-member": "Olet jo tiimissä.",
+ "message.team-not-found": "Tiimiä ei löydetty.",
+ "message.team-websites-info": "Verkkosivuja voi tarkastella kuka tahansa tiimin jäsen.",
"message.tracking-code": "Seurantakoodi",
- "message.transfer-team-website-to-user": "Transfer this website to your account?",
- "message.transfer-user-website-to-team": "Select the team to transfer this website to.",
- "message.transfer-website": "Transfer website ownership to your account or another team.",
- "message.triggered-event": "Triggered event",
- "message.unauthorized": "Unauthorized",
- "message.user-deleted": "User deleted.",
- "message.viewed-page": "Viewed page",
+ "message.transfer-team-website-to-user": "Siirretäänkö tämä verkkosivu tilillesi?",
+ "message.transfer-user-website-to-team": "Valitse tiimi, johon verkkosivusto siirretään.",
+ "message.transfer-website": "Siirrä verkkosivusto tiliisi tai toiselle tiimille.",
+ "message.triggered-event": "Laukaistu tapahtuma",
+ "message.unauthorized": "Ei oikeuksia",
+ "message.user-deleted": "Käyttäjä poistettu.",
+ "message.viewed-page": "Katsottu sivu",
"message.visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}"
-}
+}
\ No newline at end of file
diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json
index 7d2bf403..20b48f40 100644
--- a/src/lang/ja-JP.json
+++ b/src/lang/ja-JP.json
@@ -23,7 +23,7 @@
"label.behavior": "行動",
"label.boards": "ボード",
"label.bounce-rate": "直帰率",
- "label.breakdown": "故障",
+ "label.breakdown": "内訳",
"label.browser": "ブラウザ",
"label.browsers": "ブラウザ",
"label.campaigns": "キャンペーン",
diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json
index c6f01dd5..5490df42 100644
--- a/src/lang/zh-CN.json
+++ b/src/lang/zh-CN.json
@@ -1,11 +1,15 @@
{
"label.access-code": "访问代码",
+ "label.account": "账户",
+ "label.action": "行为",
"label.actions": "用户行为",
"label.activity": "活动日志",
"label.add": "添加",
"label.add-board": "添加看板",
"label.add-description": "添加描述",
+ "label.add-link": "添加链接",
"label.add-member": "添加成员",
+ "label.add-pixel": "添加像素",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
@@ -13,10 +17,13 @@
"label.after": "之后",
"label.all": "所有",
"label.all-time": "所有时间段",
+ "label.analysis": "分析",
"label.analytics": "分析",
+ "label.application": "应用",
"label.apply": "应用",
"label.attribution": "归因",
"label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。",
+ "label.audience": "受众",
"label.average": "平均",
"label.back": "返回",
"label.before": "之前",
@@ -29,11 +36,14 @@
"label.campaigns": "活动",
"label.cancel": "取消",
"label.change-password": "修改密码",
+ "label.channel": "渠道",
"label.channels": "渠道",
+ "label.chart": "图表",
"label.cities": "市/县",
"label.city": "市/县",
"label.clear-all": "清除全部",
"label.cohort": "队列",
+ "label.cohorts": "队列",
"label.compare": "比较",
"label.compare-dates": "比较日期",
"label.confirm": "确认",
@@ -53,6 +63,7 @@
"label.create-user": "创建用户",
"label.created": "已创建",
"label.created-by": "创建者",
+ "label.criteria": "条件",
"label.currency": "货币",
"label.current": "当前",
"label.current-password": "当前密码",
@@ -70,24 +81,28 @@
"label.delete-website": "删除网站",
"label.description": "描述",
"label.desktop": "台式机",
+ "label.destination-url": "目标URL",
"label.details": "详细信息",
"label.device": "设备",
"label.devices": "设备",
"label.direct": "直接",
"label.dismiss": "关闭",
"label.distinct-id": "唯一ID",
+ "label.documentation": "文档",
"label.does-not-contain": "不包含",
"label.does-not-include": "不包括",
"label.doest-not-exist": "不存在",
"label.domain": "域名",
+ "label.download": "下载",
"label.dropoff": "丢弃",
"label.edit": "编辑",
"label.edit-dashboard": "编辑仪表盘",
"label.edit-member": "编辑成员",
- "label.email": "Email",
+ "label.email": "邮箱",
"label.enable-share-url": "启用共享链接",
"label.end-step": "结束步骤",
"label.entry": "入口 URL",
+ "label.environment": "环境",
"label.event": "事件",
"label.event-data": "事件数据",
"label.event-name": "事件名称",
@@ -112,11 +127,13 @@
"label.greater-than": "大于",
"label.greater-than-equals": "大于或等于",
"label.grouped": "分组",
+ "label.growth": "增长",
"label.hostname": "主机名",
"label.includes": "包括",
"label.insight": "洞察",
"label.insights": "见解",
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
+ "label.invalid-url": "无效URL",
"label.is": "等于",
"label.is-false": "否",
"label.is-not": "不等于",
@@ -140,7 +157,9 @@
"label.leave-team": "离开团队",
"label.less-than": "少于",
"label.less-than-equals": "少于等于",
+ "label.link": "链接",
"label.links": "链接",
+ "label.location": "位置",
"label.login": "登录",
"label.logout": "退出",
"label.manage": "管理",
@@ -161,7 +180,7 @@
"label.none": "无",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "好的",
- "label.online": "Online",
+ "label.online": "在线",
"label.organic-search": "自然搜索",
"label.organic-shopping": "自然购物",
"label.organic-social": "自然社交",
@@ -183,19 +202,22 @@
"label.password": "密码",
"label.path": "路径",
"label.paths": "路径",
+ "label.pixel": "像素",
"label.pixels": "像素",
"label.powered-by": "由 {name} 提供支持",
+ "label.preferences": "偏好",
"label.previous": "先前",
"label.previous-period": "上一时期",
"label.previous-year": "上一年",
"label.profile": "个人资料",
+ "label.profiles": "个人资料",
"label.properties": "属性",
"label.property": "属性",
"label.queries": "查询",
"label.query": "查询",
"label.query-parameters": "查询参数",
"label.realtime": "实时",
- "label.referral": "Referral",
+ "label.referral": "来源",
"label.referrer": "来源",
"label.referrers": "来源域名",
"label.refresh": "刷新",
@@ -216,8 +238,13 @@
"label.role": "角色",
"label.run-query": "查询",
"label.save": "保存",
+ "label.save-cohort": "保存为群组",
+ "label.save-segment": "保存为细分",
+ "label.screen": "屏幕",
"label.screens": "屏幕尺寸",
"label.search": "搜索",
+ "label.segment": "细分",
+ "label.segments": "细分",
"label.select": "选择",
"label.select-date": "选择日期",
"label.select-filter": "选择筛选器",
@@ -235,6 +262,9 @@
"label.start-step": "开始步骤",
"label.steps": "步骤",
"label.sum": "总和",
+ "label.support": "支持",
+ "label.switch-account": "切换账户",
+ "label.table": "表格",
"label.tablet": "平板",
"label.tag": "标签",
"label.tags": "标签",
@@ -260,6 +290,7 @@
"label.total": "总数",
"label.total-records": "总记录数",
"label.tracking-code": "跟踪代码",
+ "label.traffic": "流量",
"label.transactions": "交易",
"label.transfer": "转移",
"label.transfer-website": "转移网站",
@@ -292,7 +323,7 @@
"label.yesterday": "昨天",
"message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。",
"message.active-users": "当前在线 {x} 位访客",
- "message.bad-request": "Bad request",
+ "message.bad-request": "请求错误",
"message.collected-data": "已收集的数据",
"message.confirm-delete": "你确定要删除 {target} 吗?",
"message.confirm-leave": "你确定要离开 {target} 吗?",
@@ -302,7 +333,7 @@
"message.delete-website-warning": "所有相关数据将会被删除。",
"message.error": "发生错误。",
"message.event-log": "{url} 上的 {event}",
- "message.forbidden": "Forbidden",
+ "message.forbidden": "禁止访问",
"message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名或密码不正确。",
"message.invalid-domain": "无效域名",
@@ -316,13 +347,13 @@
"message.no-teams": "您尚未创建任何团队。",
"message.no-users": "暂无用户。",
"message.no-websites-configured": "你还没有设置任何网站。",
- "message.not-found": "Not found",
- "message.nothing-selected": "Nothing selected.",
+ "message.not-found": "未找到",
+ "message.nothing-selected": "未选择",
"message.page-not-found": "页面未找到。",
"message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。",
"message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
"message.saved": "保存成功。",
- "message.sever-error": "Server error",
+ "message.sever-error": "服务器错误",
"message.share-url": "这是 {target} 的共享链接。",
"message.team-already-member": "你已是该团队的成员。",
"message.team-not-found": "未找到团队。",
@@ -332,7 +363,7 @@
"message.transfer-user-website-to-team": "选择要转移此网站的团队。",
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
"message.triggered-event": "触发事件",
- "message.unauthorized": "Unauthorized",
+ "message.unauthorized": "未授权",
"message.user-deleted": "用户已删除。",
"message.viewed-page": "已浏览页面",
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"
diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts
index 0395aef5..c41523d1 100644
--- a/src/lib/__tests__/detect.test.ts
+++ b/src/lib/__tests__/detect.test.ts
@@ -18,5 +18,5 @@ test('getIpAddress: Standard header', () => {
});
test('getIpAddress: No header', () => {
- expect(getIpAddress(new Headers())).toEqual(null);
+ expect(getIpAddress(new Headers())).toEqual(undefined);
});
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index ba6d8b09..832dfb60 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,7 +1,6 @@
import debug from 'debug';
import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
-import { secret } from '@/lib/crypto';
-import { getRandomChars } from '@/lib/generate';
+import { createAuthKey, secret } from '@/lib/crypto';
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
import redis from '@/lib/redis';
import { ensureArray } from '@/lib/utils';
@@ -53,7 +52,7 @@ export async function checkAuth(request: Request) {
}
export async function saveAuth(data: any, expire = 0) {
- const authKey = `auth:${getRandomChars(32)}`;
+ const authKey = `auth:${createAuthKey()}`;
if (redis.enabled) {
await redis.client.set(authKey, data);
diff --git a/src/lib/charts.ts b/src/lib/charts.ts
index 7d4208e2..1458cff8 100644
--- a/src/lib/charts.ts
+++ b/src/lib/charts.ts
@@ -11,7 +11,6 @@ export function renderDateLabels(unit: string, locale: string) {
switch (unit) {
case 'minute':
- return formatDate(d, 'h:mm', locale);
case 'hour':
return formatDate(d, 'p', locale);
case 'day':
diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts
index f2ebbb72..0a336f80 100644
--- a/src/lib/clickhouse.ts
+++ b/src/lib/clickhouse.ts
@@ -61,7 +61,7 @@ function getDateStringSQL(data: any, unit: string = 'utc', timezone?: string) {
function getDateSQL(field: string, unit: string, timezone?: string) {
if (timezone) {
- return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'))`;
+ return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'), '${timezone}')`;
}
return `toDateTime(date_trunc('${unit}', ${field}))`;
}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index e5090c3c..3da177c0 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -4,6 +4,7 @@ export const LOCALE_CONFIG = 'umami.locale';
export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme';
+export const CURRENCY_CONFIG = 'umami.currency';
export const DASHBOARD_CONFIG = 'umami.dashboard';
export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check';
@@ -25,6 +26,7 @@ export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_DATE_COMPARE = 'prev';
+export const DEFAULT_CURRENCY = 'USD';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 10000;
@@ -53,6 +55,7 @@ export const SESSION_COLUMNS = [
'country',
'city',
'region',
+ 'distinctId',
];
export const SEGMENT_TYPES = {
@@ -67,6 +70,7 @@ export const FILTER_COLUMNS = {
referrer: 'referrer_domain',
domain: 'referrer_domain',
hostname: 'hostname',
+ distinctId: 'distinct_id',
title: 'page_title',
query: 'url_query',
os: 'os',
@@ -93,6 +97,13 @@ export const EVENT_TYPE = {
pixelEvent: 4,
} as const;
+export const ENTITY_TYPE = {
+ website: 1,
+ link: 2,
+ pixel: 3,
+ board: 4,
+} as const;
+
export const DATA_TYPE = {
string: 1,
number: 2,
diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts
index a6d912b8..ee4c977f 100644
--- a/src/lib/crypto.ts
+++ b/src/lib/crypto.ts
@@ -63,3 +63,7 @@ export function uuid(...args: any) {
return process.env.USE_UUIDV7 ? v7() : v4();
}
+
+export function createAuthKey() {
+ return crypto.randomBytes(16).toString('hex');
+}
diff --git a/src/lib/date.ts b/src/lib/date.ts
index 3c1fd1b7..91af88f6 100644
--- a/src/lib/date.ts
+++ b/src/lib/date.ts
@@ -9,6 +9,7 @@ import {
differenceInCalendarMonths,
differenceInCalendarWeeks,
differenceInCalendarYears,
+ differenceInDays,
differenceInHours,
differenceInMinutes,
endOfDay,
@@ -136,7 +137,12 @@ export function parseDateValue(value: string) {
return { num: +num, unit };
}
-export function parseDateRange(value: string, locale = 'en-US', timezone?: string): DateRange {
+export function parseDateRange(
+ value: string,
+ unitValue?: string,
+ locale = 'en-US',
+ timezone?: string,
+): DateRange {
if (typeof value !== 'string') {
return null;
}
@@ -146,7 +152,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
const startDate = new Date(+startTime);
const endDate = new Date(+endTime);
- const unit = getMinimumUnit(startDate, endDate);
+ const unit = getMinimumUnit(startDate, endDate, true);
return {
startDate,
@@ -169,14 +175,14 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
endDate: endOfHour(now),
offset: 0,
num: num || 1,
- unit,
+ unit: unitValue || unit,
value,
};
case 'day':
return {
startDate: num ? subDays(startOfDay(now), num) : startOfDay(now),
endDate: endOfDay(now),
- unit: num ? 'day' : 'hour',
+ unit: unitValue ? unitValue : num ? 'day' : 'hour',
offset: 0,
num: num || 1,
value,
@@ -187,7 +193,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
? subWeeks(startOfWeek(now, { locale: dateLocale }), num)
: startOfWeek(now, { locale: dateLocale }),
endDate: endOfWeek(now, { locale: dateLocale }),
- unit: 'day',
+ unit: unitValue || 'day',
offset: 0,
num: num || 1,
value,
@@ -196,7 +202,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
return {
startDate: num ? subMonths(startOfMonth(now), num) : startOfMonth(now),
endDate: endOfMonth(now),
- unit: num ? 'month' : 'day',
+ unit: unitValue ? unitValue : num ? 'month' : 'day',
offset: 0,
num: num || 1,
value,
@@ -205,7 +211,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
return {
startDate: num ? subYears(startOfYear(now), num) : startOfYear(now),
endDate: endOfYear(now),
- unit: 'month',
+ unit: unitValue || 'month',
offset: 0,
num: num || 1,
value,
@@ -273,12 +279,20 @@ export function getAllowedUnits(startDate: Date, endDate: Date) {
return index >= 0 ? units.splice(index) : [];
}
-export function getMinimumUnit(startDate: number | Date, endDate: number | Date) {
+export function getMinimumUnit(
+ startDate: number | Date,
+ endDate: number | Date,
+ isDateRange: boolean = false,
+) {
if (differenceInMinutes(endDate, startDate) <= 60) {
return 'minute';
- } else if (differenceInHours(endDate, startDate) <= 48) {
+ } else if (
+ isDateRange
+ ? differenceInHours(endDate, startDate) <= 48
+ : differenceInDays(endDate, startDate) <= 30
+ ) {
return 'hour';
- } else if (differenceInCalendarMonths(endDate, startDate) <= 6) {
+ } else if (differenceInCalendarMonths(endDate, startDate) <= 7) {
return 'day';
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
return 'month';
diff --git a/src/lib/detect.ts b/src/lib/detect.ts
index 68cb6672..910d122b 100644
--- a/src/lib/detect.ts
+++ b/src/lib/detect.ts
@@ -28,6 +28,12 @@ const PROVIDER_HEADERS = [
regionHeader: 'cloudfront-viewer-country-region',
cityHeader: 'cloudfront-viewer-city',
},
+ // EdgeOne headers (requires custom request headers in Rule Priorities, see: https://edgeone.ai/document/46151)
+ {
+ countryHeader: 'eo-ipcountry',
+ regionHeader: 'eo-region-code',
+ cityHeader: 'eo-ipcity',
+ },
];
export function getDevice(userAgent: string, screen: string = '') {
diff --git a/src/lib/entity.ts b/src/lib/entity.ts
new file mode 100644
index 00000000..fd26252d
--- /dev/null
+++ b/src/lib/entity.ts
@@ -0,0 +1,12 @@
+import type { Link, Pixel, Website } from '@/generated/prisma/client';
+import { getLink, getPixel, getWebsite } from '@/queries/prisma';
+
+export async function getEntity(entityId: string): Promise {
+ const website = await getWebsite(entityId);
+ const link = await getLink(entityId);
+ const pixel = await getPixel(entityId);
+
+ const entity = website || link || pixel;
+
+ return entity;
+}
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 52fd3048..035a1811 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -1,3 +1,5 @@
+import { DEFAULT_CURRENCY } from './constants';
+
export function parseTime(val: number) {
const days = ~~(val / 86400);
const hours = ~~(val / 3600) - days * 24;
@@ -94,7 +96,7 @@ export function formatCurrency(value: number, currency: string, locale = 'en-US'
// Fallback to default currency format if an error occurs
formattedValue = new Intl.NumberFormat(locale, {
style: 'currency',
- currency: 'USD',
+ currency: DEFAULT_CURRENCY,
});
}
diff --git a/src/lib/ip.ts b/src/lib/ip.ts
index 5cd77574..a0e3a825 100644
--- a/src/lib/ip.ts
+++ b/src/lib/ip.ts
@@ -1,3 +1,5 @@
+import ipaddr from 'ipaddr.js';
+
export const IP_ADDRESS_HEADERS = [
'true-client-ip', // CDN
'cf-connecting-ip', // Cloudflare
@@ -13,35 +15,87 @@ export const IP_ADDRESS_HEADERS = [
'x-forwarded',
];
+/**
+ * Normalize IP strings to a canonical form:
+ * - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
+ * - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js)
+ */
+function normalizeIp(ip?: string | null) {
+ if (!ip) return ip;
+
+ try {
+ const parsed = ipaddr.parse(ip);
+
+ if (parsed.kind() === 'ipv6' && (parsed as ipaddr.IPv6).isIPv4MappedAddress()) {
+ return (parsed as ipaddr.IPv6).toIPv4Address().toString();
+ }
+
+ return parsed.toString();
+ } catch {
+ // Fallback: return original if parsing fails
+ return ip;
+ }
+}
+
+function resolveIp(ip?: string | null) {
+ if (!ip) return ip;
+
+ // First, try as-is
+ const normalized = normalizeIp(ip);
+ try {
+ ipaddr.parse(normalized);
+ return normalized;
+ } catch {
+ // try stripping port (handles IPv4:port; leaves IPv6 intact)
+ const stripped = stripPort(ip);
+ if (stripped !== ip) {
+ const normalizedStripped = normalizeIp(stripped);
+ try {
+ ipaddr.parse(normalizedStripped);
+ return normalizedStripped;
+ } catch {
+ return normalizedStripped;
+ }
+ }
+
+ return normalized;
+ }
+}
+
export function getIpAddress(headers: Headers) {
const customHeader = process.env.CLIENT_IP_HEADER;
if (customHeader && headers.get(customHeader)) {
- return headers.get(customHeader);
+ return resolveIp(headers.get(customHeader));
}
- const header = IP_ADDRESS_HEADERS.find(name => {
- return headers.get(name);
- });
+ const header = IP_ADDRESS_HEADERS.find(name => headers.get(name));
+ if (!header) {
+ return undefined;
+ }
const ip = headers.get(header);
if (header === 'x-forwarded-for') {
- return ip?.split(',')?.[0]?.trim();
+ return resolveIp(ip?.split(',')?.[0]?.trim());
}
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
if (match) {
- return match[1];
+ return resolveIp(match[1]);
}
}
- return ip;
+ return resolveIp(ip);
}
-export function stripPort(ip: string) {
+export function stripPort(ip?: string | null) {
+ if (!ip) {
+ return ip;
+ }
+
if (ip.startsWith('[')) {
const endBracket = ip.indexOf(']');
if (endBracket !== -1) {
diff --git a/src/lib/load.ts b/src/lib/load.ts
index d4d6c3c7..bf527975 100644
--- a/src/lib/load.ts
+++ b/src/lib/load.ts
@@ -38,3 +38,9 @@ export async function fetchSession(websiteId: string, sessionId: string): Promis
return session;
}
+
+export async function fetchAccount(userId: string) {
+ const account = await redis.client.get(`account:${userId}`);
+
+ return account;
+}
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index 64cb870f..bfd007d1 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -74,15 +74,21 @@ function getSearchSQL(column: string, param: string = 'search'): string {
function mapFilter(column: string, operator: string, name: string, type: string = '') {
const value = `{{${name}${type ? `::${type}` : ''}}}`;
+ if (name.startsWith('cohort_')) {
+ name = name.slice('cohort_'.length);
+ }
+
+ const table = SESSION_COLUMNS.includes(name) ? 'session' : 'website_event';
+
switch (operator) {
case OPERATORS.equals:
- return `${column} = ${value}`;
+ return `${table}.${column} = ${value}`;
case OPERATORS.notEquals:
- return `${column} != ${value}`;
+ return `${table}.${column} != ${value}`;
case OPERATORS.contains:
- return `${column} ilike ${value}`;
+ return `${table}.${column} ilike ${value}`;
case OPERATORS.doesNotContain:
- return `${column} not ilike ${value}`;
+ return `${table}.${column} not ilike ${value}`;
default:
return '';
}
diff --git a/src/lib/request.ts b/src/lib/request.ts
index 42c44904..7f9163cc 100644
--- a/src/lib/request.ts
+++ b/src/lib/request.ts
@@ -1,8 +1,9 @@
+import { startOfMonth, subMonths } from 'date-fns';
import { z } from 'zod';
import { checkAuth } from '@/lib/auth';
import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS } from '@/lib/constants';
import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date';
-import { fetchWebsite } from '@/lib/load';
+import { fetchAccount, fetchWebsite } from '@/lib/load';
import { filtersArrayToObject } from '@/lib/params';
import { badRequest, unauthorized } from '@/lib/response';
import type { QueryFilters } from '@/lib/types';
@@ -16,7 +17,7 @@ export async function parseRequest(
const url = new URL(request.url);
let query = Object.fromEntries(url.searchParams);
let body = await getJsonBody(request);
- let error: () => undefined | undefined;
+ let error: () => undefined | undefined | Response;
let auth = null;
if (schema) {
@@ -82,6 +83,15 @@ export function getRequestFilters(query: Record) {
export async function setWebsiteDate(websiteId: string, data: Record) {
const website = await fetchWebsite(websiteId);
+ const cloudMode = !!process.env.CLOUD_MODE;
+
+ if (cloudMode && website && !website.teamId) {
+ const account = await fetchAccount(website.userId);
+
+ if (!account?.hasSubscription) {
+ data.startDate = maxDate(data.startDate, startOfMonth(subMonths(new Date(), 6)));
+ }
+ }
if (website?.resetAt) {
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 38f7339a..a3c56a0f 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -20,7 +20,7 @@ export const dateRangeParams = {
endDate: z.coerce.date().optional(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
- compare: z.string().optional(),
+ compare: z.enum(['prev', 'yoy']).optional(),
};
export const filterParams = {
@@ -36,6 +36,7 @@ export const filterParams = {
city: z.string().optional(),
tag: z.string().optional(),
hostname: z.string().optional(),
+ distinctId: z.string().optional(),
language: z.string().optional(),
event: z.string().optional(),
segment: z.uuid().optional(),
@@ -89,6 +90,7 @@ export const fieldsParam = z.enum([
'city',
'tag',
'hostname',
+ 'distinctId',
'language',
'event',
]);
@@ -104,6 +106,23 @@ export const reportTypeParam = z.enum([
'utm',
]);
+export const operatorParam = z.enum([
+ 'eq',
+ 'neq',
+ 's',
+ 'ns',
+ 'c',
+ 'dnc',
+ 't',
+ 'f',
+ 'gt',
+ 'lt',
+ 'gte',
+ 'lte',
+ 'bf',
+ 'af',
+]);
+
export const goalReportSchema = z.object({
type: z.literal('goal'),
parameters: z
@@ -149,6 +168,7 @@ export const journeyReportSchema = z.object({
steps: z.coerce.number().min(2).max(7),
startStep: z.string().optional(),
endStep: z.string().optional(),
+ eventType: z.coerce.number().int().positive().optional(),
}),
});
@@ -157,7 +177,7 @@ export const retentionReportSchema = z.object({
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
- timezone: z.string().optional(),
+ timezone: timezoneParam.optional(),
}),
});
@@ -174,7 +194,8 @@ export const revenueReportSchema = z.object({
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
- timezone: z.string().optional(),
+ unit: unitParam.optional(),
+ timezone: timezoneParam.optional(),
currency: z.string(),
}),
});
@@ -230,3 +251,22 @@ export const reportResultSchema = z.intersection(
);
export const segmentTypeParam = z.enum(['segment', 'cohort']);
+
+export const segmentParamSchema = z.object({
+ filters: z
+ .array(
+ z.object({
+ name: z.string(),
+ operator: operatorParam,
+ value: z.string(),
+ }),
+ )
+ .optional(),
+ dateRange: z.string().optional(),
+ action: z
+ .object({
+ type: z.string(),
+ value: z.string(),
+ })
+ .optional(),
+});
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 9c061979..f3466734 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -141,3 +141,9 @@ export interface ApiError extends Error {
code?: string;
message: string;
}
+
+export interface WhiteLabel {
+ name: string;
+ url: string;
+ image: string;
+}
diff --git a/src/permissions/entity.ts b/src/permissions/entity.ts
new file mode 100644
index 00000000..bab804dd
--- /dev/null
+++ b/src/permissions/entity.ts
@@ -0,0 +1,77 @@
+import { hasPermission } from '@/lib/auth';
+import { PERMISSIONS } from '@/lib/constants';
+import { getEntity } from '@/lib/entity';
+import type { Auth } from '@/lib/types';
+import { getTeamUser } from '@/queries/prisma';
+
+export async function canViewEntity({ user }: Auth, entityId: string) {
+ if (!user) {
+ return false;
+ }
+
+ if (user.isAdmin) {
+ return true;
+ }
+
+ const entity = await getEntity(entityId);
+
+ if (entity.userId) {
+ return user.id === entity.userId;
+ }
+
+ if (entity.teamId) {
+ const teamUser = await getTeamUser(entity.teamId, user.id);
+
+ return !!teamUser;
+ }
+
+ return false;
+}
+
+export async function canUpdateEntity({ user }: Auth, entityId: string) {
+ if (!user) {
+ return false;
+ }
+
+ if (user.isAdmin) {
+ return true;
+ }
+
+ const entity = await getEntity(entityId);
+
+ if (entity.userId) {
+ return user.id === entity.userId;
+ }
+
+ if (entity.teamId) {
+ const teamUser = await getTeamUser(entity.teamId, user.id);
+
+ return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
+ }
+
+ return false;
+}
+
+export async function canDeleteEntity({ user }: Auth, entityId: string) {
+ if (!user) {
+ return false;
+ }
+
+ if (user.isAdmin) {
+ return true;
+ }
+
+ const entity = await getEntity(entityId);
+
+ if (entity.userId) {
+ return user.id === entity.userId;
+ }
+
+ if (entity.teamId) {
+ const teamUser = await getTeamUser(entity.teamId, user.id);
+
+ return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
+ }
+
+ return false;
+}
diff --git a/src/permissions/index.ts b/src/permissions/index.ts
index a70808e6..475cdaa4 100644
--- a/src/permissions/index.ts
+++ b/src/permissions/index.ts
@@ -1,3 +1,4 @@
+export * from './entity';
export * from './link';
export * from './pixel';
export * from './report';
diff --git a/src/permissions/link.ts b/src/permissions/link.ts
index c027a0b6..8dd1d7c6 100644
--- a/src/permissions/link.ts
+++ b/src/permissions/link.ts
@@ -4,7 +4,11 @@ import type { Auth } from '@/lib/types';
import { getLink, getTeamUser } from '@/queries/prisma';
export async function canViewLink({ user }: Auth, linkId: string) {
- if (user?.isAdmin) {
+ if (!user) {
+ return false;
+ }
+
+ if (user.isAdmin) {
return true;
}
@@ -24,6 +28,10 @@ export async function canViewLink({ user }: Auth, linkId: string) {
}
export async function canUpdateLink({ user }: Auth, linkId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -44,6 +52,10 @@ export async function canUpdateLink({ user }: Auth, linkId: string) {
}
export async function canDeleteLink({ user }: Auth, linkId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
diff --git a/src/permissions/pixel.ts b/src/permissions/pixel.ts
index 2131874f..14b69ace 100644
--- a/src/permissions/pixel.ts
+++ b/src/permissions/pixel.ts
@@ -4,7 +4,11 @@ import type { Auth } from '@/lib/types';
import { getPixel, getTeamUser } from '@/queries/prisma';
export async function canViewPixel({ user }: Auth, pixelId: string) {
- if (user?.isAdmin) {
+ if (!user) {
+ return false;
+ }
+
+ if (user.isAdmin) {
return true;
}
@@ -24,6 +28,10 @@ export async function canViewPixel({ user }: Auth, pixelId: string) {
}
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -44,6 +52,10 @@ export async function canUpdatePixel({ user }: Auth, pixelId: string) {
}
export async function canDeletePixel({ user }: Auth, pixelId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
diff --git a/src/permissions/report.ts b/src/permissions/report.ts
index 01b54769..5f9da019 100644
--- a/src/permissions/report.ts
+++ b/src/permissions/report.ts
@@ -3,11 +3,11 @@ import type { Auth } from '@/lib/types';
import { canViewWebsite } from './website';
export async function canViewReport(auth: Auth, report: Report) {
- if (auth.user.isAdmin) {
+ if (auth.user?.isAdmin) {
return true;
}
- if (auth.user.id === report.userId) {
+ if (auth.user?.id === report.userId) {
return true;
}
@@ -15,6 +15,10 @@ export async function canViewReport(auth: Auth, report: Report) {
}
export async function canUpdateReport({ user }: Auth, report: Report) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
diff --git a/src/permissions/team.ts b/src/permissions/team.ts
index 0f07c1a4..130290af 100644
--- a/src/permissions/team.ts
+++ b/src/permissions/team.ts
@@ -4,6 +4,10 @@ import type { Auth } from '@/lib/types';
import { getTeamUser } from '@/queries/prisma';
export async function canViewTeam({ user }: Auth, teamId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -12,14 +16,22 @@ export async function canViewTeam({ user }: Auth, teamId: string) {
}
export async function canCreateTeam({ user }: Auth) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
- return !!user;
+ return hasPermission(user.role, PERMISSIONS.teamCreate);
}
export async function canUpdateTeam({ user }: Auth, teamId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -30,6 +42,10 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
}
export async function canDeleteTeam({ user }: Auth, teamId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -40,6 +56,10 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
}
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -54,6 +74,10 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
}
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -64,5 +88,5 @@ export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
}
export async function canViewAllTeams({ user }: Auth) {
- return user.isAdmin;
+ return user?.isAdmin ?? false;
}
diff --git a/src/permissions/user.ts b/src/permissions/user.ts
index 2ed8f276..8aa453ae 100644
--- a/src/permissions/user.ts
+++ b/src/permissions/user.ts
@@ -1,10 +1,14 @@
import type { Auth } from '@/lib/types';
export async function canCreateUser({ user }: Auth) {
- return user.isAdmin;
+ return user?.isAdmin ?? false;
}
export async function canViewUser({ user }: Auth, viewedUserId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -13,10 +17,14 @@ export async function canViewUser({ user }: Auth, viewedUserId: string) {
}
export async function canViewUsers({ user }: Auth) {
- return user.isAdmin;
+ return user?.isAdmin ?? false;
}
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -25,5 +33,5 @@ export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
}
export async function canDeleteUser({ user }: Auth) {
- return user.isAdmin;
+ return user?.isAdmin ?? false;
}
diff --git a/src/permissions/website.ts b/src/permissions/website.ts
index 97952eed..9ad25ae5 100644
--- a/src/permissions/website.ts
+++ b/src/permissions/website.ts
@@ -1,7 +1,8 @@
import { hasPermission } from '@/lib/auth';
import { PERMISSIONS } from '@/lib/constants';
+import { getEntity } from '@/lib/entity';
import type { Auth } from '@/lib/types';
-import { getLink, getPixel, getTeamUser, getWebsite } from '@/queries/prisma';
+import { getTeamUser, getWebsite } from '@/queries/prisma';
export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) {
if (user?.isAdmin) {
@@ -12,13 +13,9 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return true;
}
- const website = await getWebsite(websiteId);
- const link = await getLink(websiteId);
- const pixel = await getPixel(websiteId);
+ const entity = await getEntity(websiteId);
- const entity = website || link || pixel;
-
- if (!entity) {
+ if (!entity || !user) {
return false;
}
@@ -36,10 +33,14 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
}
export async function canViewAllWebsites({ user }: Auth) {
- return user.isAdmin;
+ return user?.isAdmin ?? false;
}
export async function canCreateWebsite({ user }: Auth) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -48,6 +49,10 @@ export async function canCreateWebsite({ user }: Auth) {
}
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -72,6 +77,10 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
}
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
+ if (!user) {
+ return false;
+ }
+
if (user.isAdmin) {
return true;
}
@@ -96,6 +105,10 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
}
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
+ if (!user) {
+ return false;
+ }
+
const website = await getWebsite(websiteId);
if (!website) {
@@ -112,6 +125,10 @@ export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string
}
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
+ if (!user) {
+ return false;
+ }
+
const website = await getWebsite(websiteId);
if (!website) {
diff --git a/src/queries/prisma/index.ts b/src/queries/prisma/index.ts
index b9730f51..4dedb2b5 100644
--- a/src/queries/prisma/index.ts
+++ b/src/queries/prisma/index.ts
@@ -2,6 +2,7 @@ export * from './link';
export * from './pixel';
export * from './report';
export * from './segment';
+export * from './share';
export * from './team';
export * from './teamUser';
export * from './user';
diff --git a/src/queries/prisma/share.ts b/src/queries/prisma/share.ts
new file mode 100644
index 00000000..53246ffb
--- /dev/null
+++ b/src/queries/prisma/share.ts
@@ -0,0 +1,64 @@
+import type { Prisma } from '@/generated/prisma/client';
+import prisma from '@/lib/prisma';
+import type { QueryFilters } from '@/lib/types';
+
+export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
+ return prisma.client.share.findUnique(criteria);
+}
+
+export async function getShare(shareId: string) {
+ return findShare({
+ where: {
+ id: shareId,
+ },
+ });
+}
+
+export async function getShareByCode(slug: string) {
+ return findShare({
+ where: {
+ slug,
+ },
+ });
+}
+
+export async function getSharesByEntityId(entityId: string, filters?: QueryFilters) {
+ const { pagedQuery } = prisma;
+
+ return pagedQuery(
+ 'share',
+ {
+ where: {
+ entityId,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ },
+ filters,
+ );
+}
+
+export async function createShare(
+ data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
+) {
+ return prisma.client.share.create({
+ data,
+ });
+}
+
+export async function updateShare(
+ shareId: string,
+ data: Prisma.ShareUpdateInput | Prisma.ShareUncheckedUpdateInput,
+) {
+ return prisma.client.share.update({
+ where: {
+ id: shareId,
+ },
+ data,
+ });
+}
+
+export async function deleteShare(shareId: string) {
+ return prisma.client.share.delete({ where: { id: shareId } });
+}
diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts
index 14376fc2..467ea1e0 100644
--- a/src/queries/prisma/user.ts
+++ b/src/queries/prisma/user.ts
@@ -18,7 +18,7 @@ async function findUser(criteria: Prisma.UserFindUniqueArgs, options: GetUserOpt
...criteria,
where: {
...criteria.where,
- ...(showDeleted && { deletedAt: null }),
+ ...(showDeleted ? {} : { deletedAt: null }),
},
select: {
id: true,
diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts
index 79cb7247..fe57589c 100644
--- a/src/queries/prisma/website.ts
+++ b/src/queries/prisma/website.ts
@@ -16,15 +16,6 @@ export async function getWebsite(websiteId: string) {
});
}
-export async function getSharedWebsite(shareId: string) {
- return findWebsite({
- where: {
- shareId,
- deletedAt: null,
- },
- });
-}
-
export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) {
const { search } = filters;
const { getSearchParameters, pagedQuery } = prisma;
@@ -132,42 +123,46 @@ export async function updateWebsite(
}
export async function resetWebsite(websiteId: string) {
- const { client, transaction } = prisma;
+ const { transaction } = prisma;
const cloudMode = !!process.env.CLOUD_MODE;
return transaction(
- [
- client.revenue.deleteMany({
+ async tx => {
+ await tx.revenue.deleteMany({
where: { websiteId },
- }),
- client.eventData.deleteMany({
+ });
+
+ await tx.eventData.deleteMany({
where: { websiteId },
- }),
- client.sessionData.deleteMany({
+ });
+
+ await tx.sessionData.deleteMany({
where: { websiteId },
- }),
- client.websiteEvent.deleteMany({
+ });
+
+ await tx.websiteEvent.deleteMany({
where: { websiteId },
- }),
- client.session.deleteMany({
+ });
+
+ await tx.session.deleteMany({
where: { websiteId },
- }),
- client.website.update({
+ });
+
+ const website = await tx.website.update({
where: { id: websiteId },
data: {
resetAt: new Date(),
},
- }),
- ],
+ });
+
+ return website;
+ },
{
timeout: 30000,
},
).then(async data => {
if (cloudMode) {
- await redis.client.set(
- `website:${websiteId}`,
- data.find(website => website.id),
- );
+ await redis.client.set(`website:${websiteId}`, data);
}
return data;
@@ -175,43 +170,52 @@ export async function resetWebsite(websiteId: string) {
}
export async function deleteWebsite(websiteId: string) {
- const { client, transaction } = prisma;
+ const { transaction } = prisma;
const cloudMode = !!process.env.CLOUD_MODE;
return transaction(
- [
- client.revenue.deleteMany({
+ async tx => {
+ await tx.revenue.deleteMany({
where: { websiteId },
- }),
- client.eventData.deleteMany({
+ });
+
+ await tx.eventData.deleteMany({
where: { websiteId },
- }),
- client.sessionData.deleteMany({
+ });
+
+ await tx.sessionData.deleteMany({
where: { websiteId },
- }),
- client.websiteEvent.deleteMany({
+ });
+
+ await tx.websiteEvent.deleteMany({
where: { websiteId },
- }),
- client.session.deleteMany({
+ });
+
+ await tx.session.deleteMany({
where: { websiteId },
- }),
- client.report.deleteMany({
+ });
+
+ await tx.report.deleteMany({
where: { websiteId },
- }),
- client.segment.deleteMany({
+ });
+
+ await tx.segment.deleteMany({
where: { websiteId },
- }),
- cloudMode
- ? client.website.update({
+ });
+
+ const website = cloudMode
+ ? await tx.website.update({
data: {
deletedAt: new Date(),
},
where: { id: websiteId },
})
- : client.website.delete({
+ : await tx.website.delete({
where: { id: websiteId },
- }),
- ],
+ });
+
+ return website;
+ },
{
timeout: 30000,
},
diff --git a/src/queries/sql/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts
index 6c8f12c1..8ed6633e 100644
--- a/src/queries/sql/events/getEventDataEvents.ts
+++ b/src/queries/sql/events/getEventDataEvents.ts
@@ -93,11 +93,15 @@ async function clickhouseQuery(
string_value as propertyValue,
count(*) as total
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
@@ -120,11 +124,15 @@ async function clickhouseQuery(
data_type as dataType,
count(*) as total
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
diff --git a/src/queries/sql/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts
index 93377690..70945492 100644
--- a/src/queries/sql/events/getEventDataFields.ts
+++ b/src/queries/sql/events/getEventDataFields.ts
@@ -65,11 +65,15 @@ async function clickhouseQuery(
string_value) as "value",
count(*) as "total"
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts
index 82c078f8..82d0b6b6 100644
--- a/src/queries/sql/events/getEventDataProperties.ts
+++ b/src/queries/sql/events/getEventDataProperties.ts
@@ -69,11 +69,15 @@ async function clickhouseQuery(
data_key as propertyName,
count(*) as total
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
diff --git a/src/queries/sql/events/getEventDataStats.ts b/src/queries/sql/events/getEventDataStats.ts
index 89e13582..9d178f67 100644
--- a/src/queries/sql/events/getEventDataStats.ts
+++ b/src/queries/sql/events/getEventDataStats.ts
@@ -72,11 +72,15 @@ async function clickhouseQuery(
data_key,
count(*) as "total"
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts
index 0426e646..7c1ae282 100644
--- a/src/queries/sql/events/getEventDataValues.ts
+++ b/src/queries/sql/events/getEventDataValues.ts
@@ -72,11 +72,15 @@ async function clickhouseQuery(
string_value) as "value",
count(*) as "total"
from event_data
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.event_id = event_data.event_id
+ and website_event.session_id = event_data.session_id
and website_event.website_id = event_data.website_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
diff --git a/src/queries/sql/events/getEventExpandedMetrics.ts b/src/queries/sql/events/getEventExpandedMetrics.ts
index f03a347d..86bda850 100644
--- a/src/queries/sql/events/getEventExpandedMetrics.ts
+++ b/src/queries/sql/events/getEventExpandedMetrics.ts
@@ -58,7 +58,7 @@ async function relationalQuery(
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
- ${column} name,
+ ${column} as "name",
website_event.session_id,
website_event.visit_id,
count(*) as "c",
@@ -72,6 +72,7 @@ async function relationalQuery(
${filterQuery}
group by name, website_event.session_id, website_event.visit_id
) as t
+ where name != ''
group by name
order by visitors desc, visits desc
limit ${limit}
diff --git a/src/queries/sql/events/getWebsiteEventStats.ts b/src/queries/sql/events/getWebsiteEventStats.ts
new file mode 100644
index 00000000..27179d10
--- /dev/null
+++ b/src/queries/sql/events/getWebsiteEventStats.ts
@@ -0,0 +1,97 @@
+import clickhouse from '@/lib/clickhouse';
+import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
+import prisma from '@/lib/prisma';
+import type { QueryFilters } from '@/lib/types';
+
+const FUNCTION_NAME = 'getWebsiteEventStats';
+
+export interface WebsiteEventStatsData {
+ events: number;
+ visitors: number;
+ visits: number;
+ uniqueEvents: number;
+}
+
+export async function getWebsiteEventStats(
+ ...args: [websiteId: string, filters: QueryFilters]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ filters: QueryFilters,
+): Promise {
+ const { parseFilters, rawQuery } = prisma;
+ const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ });
+
+ return rawQuery(
+ `
+ select
+ cast(coalesce(sum(t.c), 0) as bigint) as "events",
+ count(distinct t.session_id) as "visitors",
+ count(distinct t.visit_id) as "visits",
+ count(distinct t.event_name) as "uniqueEvents"
+ from (
+ select
+ website_event.session_id,
+ website_event.visit_id,
+ website_event.event_name,
+ count(*) as "c"
+ from website_event
+ ${cohortQuery}
+ ${joinSessionQuery}
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ and website_event.event_type = 2
+ ${filterQuery}
+ group by 1, 2, 3
+ ) as t
+ `,
+ queryParams,
+ FUNCTION_NAME,
+ ).then(result => result?.[0]);
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ filters: QueryFilters,
+): Promise {
+ const { rawQuery, parseFilters } = clickhouse;
+ const { filterQuery, cohortQuery, queryParams } = parseFilters({
+ ...filters,
+ websiteId,
+ });
+
+ return rawQuery(
+ `
+ select
+ sum(t.c) as "events",
+ uniq(t.session_id) as "visitors",
+ uniq(t.visit_id) as "visits",
+ count(distinct t.event_name) as "uniqueEvents"
+ from (
+ select
+ session_id,
+ visit_id,
+ event_name,
+ count(*) c
+ from website_event
+ ${cohortQuery}
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2
+ ${filterQuery}
+ group by session_id, visit_id, event_name
+ ) as t;
+ `,
+ queryParams,
+ FUNCTION_NAME,
+ ).then(result => result?.[0]);
+}
diff --git a/src/queries/sql/getChannelExpandedMetrics.ts b/src/queries/sql/getChannelExpandedMetrics.ts
index 33640d59..f674d182 100644
--- a/src/queries/sql/getChannelExpandedMetrics.ts
+++ b/src/queries/sql/getChannelExpandedMetrics.ts
@@ -89,7 +89,7 @@ async function relationalQuery(
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
when ${toPostgresPositionClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
when ${toPostgresPositionClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
- else '' end AS name,
+ else '' end as "name",
session_id,
visit_id,
c,
diff --git a/src/queries/sql/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts
index 075b65e2..c847b6f7 100644
--- a/src/queries/sql/getRealtimeActivity.ts
+++ b/src/queries/sql/getRealtimeActivity.ts
@@ -30,7 +30,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
session.device,
session.country,
website_event.url_path as "urlPath",
- website_event.referrer_domain as "referrerDomain"
+ website_event.referrer_domain as "referrerDomain",
+ website_event.hostname
from website_event
${cohortQuery}
inner join session
@@ -65,7 +66,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
device,
country,
url_path as urlPath,
- referrer_domain as referrerDomain
+ referrer_domain as referrerDomain,
+ hostname
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
diff --git a/src/queries/sql/getWeeklyTraffic.ts b/src/queries/sql/getWeeklyTraffic.ts
index 7bbe78a7..1868b922 100644
--- a/src/queries/sql/getWeeklyTraffic.ts
+++ b/src/queries/sql/getWeeklyTraffic.ts
@@ -14,7 +14,7 @@ export async function getWeeklyTraffic(...args: [websiteId: string, filters: Que
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
- const timezone = 'utc';
+ const { timezone = 'utc' } = filters;
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
@@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by time
- order by 2
+ order by 1
`,
queryParams,
FUNCTION_NAME,
diff --git a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
index 986d7d5a..ccb0be53 100644
--- a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
+++ b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts
@@ -86,7 +86,7 @@ async function relationalQuery(
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
- ${column} as name,
+ ${column} as "name",
website_event.session_id,
website_event.visit_id,
count(*) as "c",
diff --git a/src/queries/sql/reports/getAttribution.ts b/src/queries/sql/reports/getAttribution.ts
index 1d040781..29068f7d 100644
--- a/src/queries/sql/reports/getAttribution.ts
+++ b/src/queries/sql/reports/getAttribution.ts
@@ -52,8 +52,8 @@ async function relationalQuery(
function getUTMQuery(utmColumn: string) {
return `
select
- coalesce(we.${utmColumn}, '') name,
- ${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
+ coalesce(we.${utmColumn}, '') as "name",
+ ${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} as "value"
from model m
join website_event we
on we.created_at = m.created_at
@@ -128,7 +128,7 @@ async function relationalQuery(
`
${currency ? revenueEventQuery : eventQuery}
${getModelQuery(model)}
- select coalesce(we.referrer_domain, '') name,
+ select coalesce(we.referrer_domain, '') as "name",
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
from model m
join website_event we
@@ -166,8 +166,8 @@ async function relationalQuery(
when coalesce(li_fat_id, '') != '' then 'LinkedIn Ads'
when coalesce(twclid, '') != '' then 'Twitter Ads (X)'
else ''
- end name,
- ${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
+ end as "name",
+ ${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} as "value"
from model m
join website_event we
on we.created_at = m.created_at
diff --git a/src/queries/sql/reports/getBreakdown.ts b/src/queries/sql/reports/getBreakdown.ts
index 51773d86..c84db769 100644
--- a/src/queries/sql/reports/getBreakdown.ts
+++ b/src/queries/sql/reports/getBreakdown.ts
@@ -131,5 +131,5 @@ function parseFields(fields: string[]) {
}
function parseFieldsByName(fields: string[]) {
- return `${fields.map(name => name).join(',')}`;
+ return `${fields.map(name => `"${name}"`).join(',')}`;
}
diff --git a/src/queries/sql/reports/getJourney.ts b/src/queries/sql/reports/getJourney.ts
index 283e0fad..d12d371b 100644
--- a/src/queries/sql/reports/getJourney.ts
+++ b/src/queries/sql/reports/getJourney.ts
@@ -60,7 +60,7 @@ async function relationalQuery(
endStepQuery: string;
params: Record;
} {
- const params = {};
+ const params: { startStep?: string; endStep?: string } = {};
let sequenceQuery = '';
let startStepQuery = '';
let endStepQuery = '';
@@ -119,7 +119,7 @@ async function relationalQuery(
select distinct
website_event.visit_id,
website_event.referrer_path,
- coalesce(nullIf(website_event.event_name, ''), website_event.url_path) event,
+ coalesce(nullIf(website_event.event_name, ''), website_event.url_path) "event",
row_number() OVER (PARTITION BY visit_id ORDER BY website_event.created_at) AS event_number
from website_event
${cohortQuery}
@@ -172,7 +172,7 @@ async function clickhouseQuery(
endStepQuery: string;
params: Record;
} {
- const params = {};
+ const params: { startStep?: string; endStep?: string } = {};
let sequenceQuery = '';
let startStepQuery = '';
let endStepQuery = '';
diff --git a/src/queries/sql/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts
index fa25078c..30d7d7f1 100644
--- a/src/queries/sql/reports/getRevenue.ts
+++ b/src/queries/sql/reports/getRevenue.ts
@@ -41,14 +41,17 @@ async function relationalQuery(
currency,
});
- const joinQuery = filterQuery
- ? `join website_event
- on website_event.website_id = revenue.website_id
- and website_event.session_id = revenue.session_id
- and website_event.event_id = revenue.event_id
- and website_event.website_id = {{websiteId::uuid}}
- and website_event.created_at between {{startDate}} and {{endDate}}`
- : '';
+ const joinQuery =
+ filterQuery || cohortQuery
+ ? `join (select *
+ from website_event
+ where website_id = {{websiteId::uuid}}
+ and created_at between {{startDate}} and {{endDate}}
+ and event_type = 2) website_event
+ on website_event.website_id = revenue.website_id
+ and website_event.session_id = revenue.session_id
+ and website_event.event_id = revenue.event_id`
+ : '';
const chart = await rawQuery(
`
@@ -62,7 +65,7 @@ async function relationalQuery(
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
- and revenue.currency = upper({{currency}})
+ and upper(revenue.currency) = {{currency}}
${filterQuery}
group by x, t
order by t
@@ -73,8 +76,8 @@ async function relationalQuery(
const country = await rawQuery(
`
select
- session.country as name,
- sum(revenue) value
+ session.country as "name",
+ sum(revenue) as "value"
from revenue
${joinQuery}
join session
@@ -83,7 +86,7 @@ async function relationalQuery(
${cohortQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
- and revenue.currency = upper({{currency}})
+ and upper(revenue.currency) = {{currency}}
${filterQuery}
group by session.country
`,
@@ -102,7 +105,7 @@ async function relationalQuery(
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
- and revenue.currency = upper({{currency}})
+ and upper(revenue.currency) = {{currency}}
${filterQuery}
`,
queryParams,
@@ -129,12 +132,15 @@ async function clickhouseQuery(
});
const joinQuery = filterQuery
- ? `join website_event
- on website_event.website_id = website_revenue.website_id
- and website_event.session_id = website_revenue.session_id
- and website_event.event_id = website_revenue.event_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}`
+ ? `any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
+ on website_event.website_id = website_revenue.website_id
+ and website_event.session_id = website_revenue.session_id
+ and website_event.event_id = website_revenue.event_id`
: '';
const chart = await rawQuery<
@@ -154,7 +160,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and website_revenue.currency = upper({currency:String})
+ and upper(website_revenue.currency) = {currency:String}
${filterQuery}
group by x, t
order by t
@@ -170,19 +176,22 @@ async function clickhouseQuery(
>(
`
select
- website_event.country as name,
- sum(website_revenue.revenue) as value
+ website_event.country as "name",
+ sum(website_revenue.revenue) as "value"
from website_revenue
- join website_event
+ any left join (
+ select *
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = 2) website_event
on website_event.website_id = website_revenue.website_id
and website_event.session_id = website_revenue.session_id
and website_event.event_id = website_revenue.event_id
- and website_event.website_id = {websiteId:UUID}
- and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and website_revenue.currency = upper({currency:String})
+ and upper(website_revenue.currency) = {currency:String}
${filterQuery}
group by website_event.country
order by value desc
@@ -205,7 +214,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
- and website_revenue.currency = upper({currency:String})
+ and upper(website_revenue.currency) = {currency:String}
${filterQuery}
`,
queryParams,
diff --git a/src/queries/sql/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts
index af31fca6..1ac7e6ff 100644
--- a/src/queries/sql/sessions/getSessionActivity.ts
+++ b/src/queries/sql/sessions/getSessionActivity.ts
@@ -29,6 +29,7 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
event_type as "eventType",
event_name as "eventName",
visit_id as "visitId",
+ hostname,
event_id IN (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
@@ -60,6 +61,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu
event_type as eventType,
event_name as eventName,
visit_id as visitId,
+ hostname,
event_id IN (select event_id
from event_data
where website_id = {websiteId:UUID}
diff --git a/src/queries/sql/sessions/getSessionExpandedMetrics.ts b/src/queries/sql/sessions/getSessionExpandedMetrics.ts
index 85c12939..6b85cd45 100644
--- a/src/queries/sql/sessions/getSessionExpandedMetrics.ts
+++ b/src/queries/sql/sessions/getSessionExpandedMetrics.ts
@@ -65,7 +65,7 @@ async function relationalQuery(
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
- ${column} name,
+ ${column} as "name",
${includeCountry ? 'country,' : ''}
website_event.session_id,
website_event.visit_id,
@@ -82,6 +82,7 @@ async function relationalQuery(
group by name, website_event.session_id, website_event.visit_id
${includeCountry ? ', country' : ''}
) as t
+ where name != ''
group by name
${includeCountry ? ', country' : ''}
order by visitors desc, visits desc
diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts
index 74093177..cce6cd28 100644
--- a/src/queries/sql/sessions/saveSessionData.ts
+++ b/src/queries/sql/sessions/saveSessionData.ts
@@ -46,31 +46,23 @@ export async function relationalQuery({
createdAt,
}));
- const existing = await client.sessionData.findMany({
- where: {
- sessionId,
- },
- select: {
- id: true,
- sessionId: true,
- dataKey: true,
- },
- });
-
for (const data of flattenedData) {
const { sessionId, dataKey, ...props } = data;
- const record = existing.find(e => e.sessionId === sessionId && e.dataKey === dataKey);
- if (record) {
- await client.sessionData.update({
- where: {
- id: record.id,
- },
- data: {
- ...props,
- },
- });
- } else {
+ // Try to update existing record using compound where clause
+ // This is safer than using id from a previous query due to race conditions
+ const updateResult = await client.sessionData.updateMany({
+ where: {
+ sessionId,
+ dataKey,
+ },
+ data: {
+ ...props,
+ },
+ });
+
+ // If no record was updated, create a new one
+ if (updateResult.count === 0) {
await client.sessionData.create({
data,
});
diff --git a/src/styles/global.css b/src/styles/global.css
index e9fca9fd..6e767563 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -41,3 +41,18 @@ a:hover {
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
}
+
+/* Fix autofill background color to match dark theme */
+input:-webkit-autofill,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:active,
+textarea:-webkit-autofill,
+textarea:-webkit-autofill:hover,
+textarea:-webkit-autofill:focus,
+select:-webkit-autofill,
+select:-webkit-autofill:hover,
+select:-webkit-autofill:focus {
+ -webkit-box-shadow: 0 0 0 1000px var(--background-color) inset !important;
+ transition: color 5000s ease-in-out 0s;
+}
diff --git a/src/tracker/index.js b/src/tracker/index.js
index ad3648ac..85d27430 100644
--- a/src/tracker/index.js
+++ b/src/tracker/index.js
@@ -12,7 +12,13 @@
if (!currentScript) return;
const { hostname, href, origin } = location;
- const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
+
+ let localStorage;
+ try {
+ localStorage = href.startsWith('data:') ? undefined : window.localStorage;
+ } catch {
+ /* (DOMException) SecurityError: Access is denied for this document. */
+ }
const _data = 'data-';
const _false = 'false';