diff --git a/.eslintrc.json b/.eslintrc.json index 9cbbd586..d0f2ba18 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,7 +42,8 @@ "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], - "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }] + "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }], + "@typescript-eslint/triple-slash-reference": "off" }, "globals": { "React": "writable" diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be084..830fb594 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 590c9174..007839a8 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@react-spring/web": "^9.7.3", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.83.0", - "@umami/react-zen": "^0.163.0", + "@umami/react-zen": "^0.164.0", "@umami/redis-client": "^0.27.0", "bcryptjs": "^3.0.2", "chalk": "^5.4.1", @@ -112,7 +112,7 @@ "lucide-react": "^0.540.0", "maxmind": "^4.3.28", "md5": "^2.3.0", - "next": "15.4.7", + "next": "15.5.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "papaparse": "^5.5.3", @@ -132,7 +132,7 @@ "thenby": "^1.3.4", "uuid": "^11.1.0", "zod": "^3.25.76", - "zustand": "^5.0.6" + "zustand": "^5.0.8" }, "devDependencies": { "@formatjs/cli": "^4.2.29", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fb9a26e..2db2f044 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.83.0 version: 5.85.3(react@19.1.1) '@umami/react-zen': - specifier: ^0.163.0 - version: 0.163.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1)) + specifier: ^0.164.0 + version: 0.164.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1)) '@umami/redis-client': specifier: ^0.27.0 version: 0.27.0 @@ -135,8 +135,8 @@ importers: specifier: ^2.3.0 version: 2.3.0 next: - specifier: 15.4.7 - version: 15.4.7(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 15.5.0 + version: 15.5.0(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) node-fetch: specifier: ^3.2.8 version: 3.3.2 @@ -195,8 +195,8 @@ importers: specifier: ^3.25.76 version: 3.25.76 zustand: - specifier: ^5.0.6 - version: 5.0.7(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: '@formatjs/cli': specifier: ^4.2.29 @@ -1382,56 +1382,56 @@ packages: resolution: {integrity: sha512-SXQY/nCiSOSAZWNls/DQxrICldUR7PHSMUw2J2/ZejH1dk12Vwd3+SzSihHrRW9PNcErZkC2g3seM7bWZlvBRg==} engines: {node: '>=18.0.0'} - '@next/env@15.4.7': - resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} + '@next/env@15.5.0': + resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==} '@next/eslint-plugin-next@14.2.31': resolution: {integrity: sha512-ouaB+l8Cr/uzGxoGHUvd01OnfFTM8qM81Crw1AG0xoWDRN0DKLXyTWVe0FdAOHVBpGuXB87aufdRmrwzZDArIw==} - '@next/swc-darwin-arm64@15.4.7': - resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==} + '@next/swc-darwin-arm64@15.5.0': + resolution: {integrity: sha512-v7Jj9iqC6enxIRBIScD/o0lH7QKvSxq2LM8UTyqJi+S2w2QzhMYjven4vgu/RzgsdtdbpkyCxBTzHl/gN5rTRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.7': - resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==} + '@next/swc-darwin-x64@15.5.0': + resolution: {integrity: sha512-s2Nk6ec+pmYmAb/utawuURy7uvyYKDk+TRE5aqLRsdnj3AhwC9IKUBmhfnLmY/+P+DnwqpeXEFIKe9tlG0p6CA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.7': - resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==} + '@next/swc-linux-arm64-gnu@15.5.0': + resolution: {integrity: sha512-mGlPJMZReU4yP5fSHjOxiTYvZmwPSWn/eF/dcg21pwfmiUCKS1amFvf1F1RkLHPIMPfocxLViNWFvkvDB14Isg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.7': - resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==} + '@next/swc-linux-arm64-musl@15.5.0': + resolution: {integrity: sha512-biWqIOE17OW/6S34t1X8K/3vb1+svp5ji5QQT/IKR+VfM3B7GvlCwmz5XtlEan2ukOUf9tj2vJJBffaGH4fGRw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.7': - resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==} + '@next/swc-linux-x64-gnu@15.5.0': + resolution: {integrity: sha512-zPisT+obYypM/l6EZ0yRkK3LEuoZqHaSoYKj+5jiD9ESHwdr6QhnabnNxYkdy34uCigNlWIaCbjFmQ8FY5AlxA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.7': - resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==} + '@next/swc-linux-x64-musl@15.5.0': + resolution: {integrity: sha512-+t3+7GoU9IYmk+N+FHKBNFdahaReoAktdOpXHFIPOU1ixxtdge26NgQEEkJkCw2dHT9UwwK5zw4mAsURw4E8jA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.7': - resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==} + '@next/swc-win32-arm64-msvc@15.5.0': + resolution: {integrity: sha512-d8MrXKh0A+c9DLiy1BUFwtg3Hu90Lucj3k6iKTUdPOv42Ve2UiIG8HYi3UAb8kFVluXxEfdpCoPPCSODk5fDcw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.7': - resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==} + '@next/swc-win32-x64-msvc@15.5.0': + resolution: {integrity: sha512-Fe1tGHxOWEyQjmygWkkXSwhFcTJuimrNu52JEuwItrKJVV4iRjbWp9I7zZjwqtiNnQmxoEvoisn8wueFLrNpvQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2549,8 +2549,8 @@ packages: resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.163.0': - resolution: {integrity: sha512-H+Z7sADljnBdzRQdOUIHXKphiPkzHKTLTNtBf/VbylzXg5A61e+OYoDG37eOkR+JFU9+KmJnF+zOiXyA33LW0A==} + '@umami/react-zen@0.164.0': + resolution: {integrity: sha512-z27uy0W3ZL0MH2cdVuu0c4guInHJQC2rYcAXxwxOAdEMtkzWym9ODfK3v5ihqS6oct+6er/bS1yVJ8gNnRvXDw==} '@umami/redis-client@0.27.0': resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} @@ -3005,6 +3005,9 @@ packages: caniuse-lite@1.0.30001735: resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + caniuse-lite@1.0.30001736: + resolution: {integrity: sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -5177,8 +5180,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.4.7: - resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==} + next@15.5.0: + resolution: {integrity: sha512-N1lp9Hatw3a9XLt0307lGB4uTKsXDhyOKQo7uYMzX4i0nF/c27grcGXkLdb7VcT8QPYLBa8ouIyEoUQJ2OyeNQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -7088,8 +7091,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zustand@5.0.7: - resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -8178,34 +8181,34 @@ snapshots: '@netlify/plugin-nextjs@5.12.0': {} - '@next/env@15.4.7': {} + '@next/env@15.5.0': {} '@next/eslint-plugin-next@14.2.31': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@15.4.7': + '@next/swc-darwin-arm64@15.5.0': optional: true - '@next/swc-darwin-x64@15.4.7': + '@next/swc-darwin-x64@15.5.0': optional: true - '@next/swc-linux-arm64-gnu@15.4.7': + '@next/swc-linux-arm64-gnu@15.5.0': optional: true - '@next/swc-linux-arm64-musl@15.4.7': + '@next/swc-linux-arm64-musl@15.5.0': optional: true - '@next/swc-linux-x64-gnu@15.4.7': + '@next/swc-linux-x64-gnu@15.5.0': optional: true - '@next/swc-linux-x64-musl@15.4.7': + '@next/swc-linux-x64-musl@15.5.0': optional: true - '@next/swc-win32-arm64-msvc@15.4.7': + '@next/swc-win32-arm64-msvc@15.5.0': optional: true - '@next/swc-win32-x64-msvc@15.4.7': + '@next/swc-win32-x64-msvc@15.5.0': optional: true '@nodelib/fs.scandir@2.1.5': @@ -9846,7 +9849,7 @@ snapshots: '@typescript-eslint/types': 8.39.1 eslint-visitor-keys: 4.2.1 - '@umami/react-zen@0.163.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1))': + '@umami/react-zen@0.164.0(@babel/core@7.28.3)(@types/react@19.1.10)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.1))': dependencies: '@fontsource/jetbrains-mono': 5.2.6 '@internationalized/date': 3.8.2 @@ -9856,14 +9859,14 @@ snapshots: glob: 10.4.5 highlight.js: 11.11.1 lucide-react: 0.511.0(react@19.1.1) - next: 15.4.7(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.5.0(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 react-aria-components: 1.9.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-dom: 19.1.1(react@19.1.1) react-hook-form: 7.62.0(react@19.1.1) react-icons: 5.5.0(react@19.1.1) thenby: 1.3.4 - zustand: 5.0.7(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + zustand: 5.0.8(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) transitivePeerDependencies: - '@babel/core' - '@opentelemetry/api' @@ -10374,12 +10377,14 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.25.2 - caniuse-lite: 1.0.30001735 + caniuse-lite: 1.0.30001736 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-lite@1.0.30001735: {} + caniuse-lite@1.0.30001736: {} + caseless@0.12.0: {} chalk@2.4.2: @@ -12959,24 +12964,24 @@ snapshots: neo-async@2.6.2: {} - next@15.4.7(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@15.5.0(@babel/core@7.28.3)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@next/env': 15.4.7 + '@next/env': 15.5.0 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001735 + caniuse-lite: 1.0.30001736 postcss: 8.4.31 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.1.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.7 - '@next/swc-darwin-x64': 15.4.7 - '@next/swc-linux-arm64-gnu': 15.4.7 - '@next/swc-linux-arm64-musl': 15.4.7 - '@next/swc-linux-x64-gnu': 15.4.7 - '@next/swc-linux-x64-musl': 15.4.7 - '@next/swc-win32-arm64-msvc': 15.4.7 - '@next/swc-win32-x64-msvc': 15.4.7 + '@next/swc-darwin-arm64': 15.5.0 + '@next/swc-darwin-x64': 15.5.0 + '@next/swc-linux-arm64-gnu': 15.5.0 + '@next/swc-linux-arm64-musl': 15.5.0 + '@next/swc-linux-x64-gnu': 15.5.0 + '@next/swc-linux-x64-musl': 15.5.0 + '@next/swc-win32-arm64-msvc': 15.5.0 + '@next/swc-win32-x64-msvc': 15.5.0 babel-plugin-react-compiler: 19.1.0-rc.2 sharp: 0.34.3 transitivePeerDependencies: @@ -15124,7 +15129,7 @@ snapshots: zod@3.25.76: {} - zustand@5.0.7(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + zustand@5.0.8(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): optionalDependencies: '@types/react': 19.1.10 immer: 9.0.21 diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index e360dae1..fe07a1b6 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -13,7 +13,7 @@ import { LayoutDashboard, Link as LinkIcon, Logo, - Grid2X2, + Pixel, Settings, PanelLeft, } from '@/components/icons'; @@ -21,6 +21,7 @@ import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; import { TeamsButton } from '@/components/input/TeamsButton'; import { PanelButton } from '@/components/input/PanelButton'; import { ProfileButton } from '@/components/input/ProfileButton'; +import { LanguageButton } from '@/components/input/LanguageButton'; export function SideNav(props: SidebarProps) { const { formatMessage, labels } = useMessages(); @@ -52,7 +53,7 @@ export function SideNav(props: SidebarProps) { id: 'pixels', label: formatMessage(labels.pixels), path: '/pixels', - icon: , + icon: , }, ]; @@ -97,6 +98,7 @@ export function SideNav(props: SidebarProps) { {!isCollapsed && !hasNav && ( + )} diff --git a/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx b/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx index a7851d78..21501973 100644 --- a/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx +++ b/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx @@ -1,6 +1,6 @@ 'use client'; import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings'; -import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { TeamProvider } from '@/app/(main)/teams/TeamProvider'; export function AdminTeamPage({ teamId }: { teamId: string }) { return ( diff --git a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx index 193d2e47..52073459 100644 --- a/src/app/(main)/admin/users/[userId]/UserEditForm.tsx +++ b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx @@ -9,15 +9,13 @@ import { PasswordField, useToast, } from '@umami/react-zen'; -import { useApi, useLoginQuery, useMessages, useModified } from '@/components/hooks'; +import { useApi, useLoginQuery, useMessages, useModified, useUser } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; -import { useContext } from 'react'; -import { UserContext } from './UserProvider'; export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) { const { formatMessage, labels, messages, getMessage } = useMessages(); const { post, useMutation } = useApi(); - const user = useContext(UserContext); + const user = useUser(); const { user: login } = useLoginQuery(); const { toast } = useToast(); const { touch } = useModified(); diff --git a/src/app/(main)/admin/users/[userId]/UserHeader.tsx b/src/app/(main)/admin/users/[userId]/UserHeader.tsx index f9fcdd64..aeaca0b2 100644 --- a/src/app/(main)/admin/users/[userId]/UserHeader.tsx +++ b/src/app/(main)/admin/users/[userId]/UserHeader.tsx @@ -1,10 +1,9 @@ -import { useContext } from 'react'; import { User } from '@/components/icons'; import { PageHeader } from '@/components/common/PageHeader'; -import { UserContext } from '@/app/(main)/admin/users/[userId]/UserProvider'; +import { useUser } from '@/components/hooks'; export function UserHeader() { - const user = useContext(UserContext); + const user = useUser(); return } />; } diff --git a/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx b/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx index 54cb9384..5da82afd 100644 --- a/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx +++ b/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx @@ -1,6 +1,6 @@ 'use client'; import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings'; -import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { Panel } from '@/components/common/Panel'; export function AdminWebsitePage({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/links/LinkEditForm.tsx b/src/app/(main)/links/LinkEditForm.tsx index a6257768..38298d9f 100644 --- a/src/app/(main)/links/LinkEditForm.tsx +++ b/src/app/(main)/links/LinkEditForm.tsx @@ -32,8 +32,8 @@ export function LinkEditForm({ onSave?: () => void; onClose?: () => void; }) { - const { formatMessage, labels } = useMessages(); - const { mutate, error, isPending, touch } = useUpdateQuery( + const { formatMessage, labels, messages } = useMessages(); + const { mutate, error, isPending, touch, toast } = useUpdateQuery( linkId ? `/links/${linkId}` : '/links', { id: linkId, @@ -48,6 +48,7 @@ export function LinkEditForm({ const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { + toast(formatMessage(messages.saved)); touch('links'); onSave?.(); onClose?.(); diff --git a/src/app/(main)/links/LinkProvider.tsx b/src/app/(main)/links/LinkProvider.tsx new file mode 100644 index 00000000..6593d85a --- /dev/null +++ b/src/app/(main)/links/LinkProvider.tsx @@ -0,0 +1,20 @@ +'use client'; +import { createContext, ReactNode } from 'react'; +import { useLinkQuery } from '@/components/hooks'; +import { Loading } from '@umami/react-zen'; + +export const LinkContext = createContext(null); + +export function LinkProvider({ linkId, children }: { linkId?: string; children: ReactNode }) { + const { data: link, isLoading, isFetching } = useLinkQuery(linkId); + + if (isFetching && isLoading) { + return ; + } + + if (!link) { + return null; + } + + return {children}; +} diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index 5e854a4a..b8471829 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -1,17 +1,16 @@ +import Link from 'next/link'; import { DataTable, DataColumn, Row } from '@umami/react-zen'; -import { useConfig, useMessages, useNavigation } from '@/components/hooks'; +import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { Empty } from '@/components/common/Empty'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; import { LinkEditButton } from './LinkEditButton'; import { LinkDeleteButton } from './LinkDeleteButton'; -import { LINKS_URL } from '@/lib/constants'; export function LinksTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { websiteId } = useNavigation(); - const { linksUrl } = useConfig(); - const hostUrl = linksUrl || LINKS_URL; + const { websiteId, renderUrl } = useNavigation(); + const { getSlugUrl } = useSlug('link'); if (data.length === 0) { return ; @@ -19,10 +18,14 @@ export function LinksTable({ data = [] }) { return ( - + + {({ id, name }: any) => { + return {name}; + }} + {({ slug }: any) => { - const url = `${hostUrl}/${slug}`; + const url = getSlugUrl(slug); return {url}; }} diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx new file mode 100644 index 00000000..3e59043c --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkControls.tsx @@ -0,0 +1,32 @@ +import { Column, Row } from '@umami/react-zen'; +import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; +import { FilterBar } from '@/components/input/FilterBar'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; +import { ExportButton } from '@/components/input/ExportButton'; + +export function LinkControls({ + linkId: websiteId, + allowFilter = true, + allowDateFilter = true, + allowMonthFilter, + allowDownload = false, +}: { + linkId: string; + allowFilter?: boolean; + allowDateFilter?: boolean; + allowMonthFilter?: boolean; + allowDownload?: boolean; +}) { + return ( + + + {allowFilter ? :
} + {allowDateFilter && } + {allowDownload && } + {allowMonthFilter && } + + {allowFilter && } + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx new file mode 100644 index 00000000..0d325ebb --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -0,0 +1,22 @@ +import { useLink, useMessages, useSlug } from '@/components/hooks'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Icon, Text } from '@umami/react-zen'; +import { ExternalLink, Link } from '@/components/icons'; +import { LinkButton } from '@/components/common/LinkButton'; + +export function LinkHeader() { + const { formatMessage, labels } = useMessages(); + const { getSlugUrl } = useSlug('link'); + const link = useLink(); + + return ( + }> + + + + + {formatMessage(labels.view)} + + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx new file mode 100644 index 00000000..5e9d6cbf --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx @@ -0,0 +1,71 @@ +import { useDateRange, useMessages } from '@/components/hooks'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; +import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; + +export function LinkMetricsBar({ + linkId, +}: { + linkId: string; + showChange?: boolean; + compareMode?: boolean; +}) { + const { dateRange } = useDateRange(linkId); + const { formatMessage, labels } = useMessages(); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); + const isAllTime = dateRange.value === 'all'; + + const { pageviews, visitors, visits, comparison } = data || {}; + + const metrics = data + ? [ + { + value: visitors, + label: formatMessage(labels.visitors), + change: visitors - comparison.visitors, + formatValue: formatLongNumber, + }, + { + value: visits, + label: formatMessage(labels.visits), + change: visits - comparison.visits, + formatValue: formatLongNumber, + }, + { + value: pageviews, + label: formatMessage(labels.views), + change: pageviews - comparison.pageviews, + formatValue: formatLongNumber, + }, + ] + : null; + + return ( + + + {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => { + return ( + + ); + })} + + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx new file mode 100644 index 00000000..a7751625 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -0,0 +1,25 @@ +'use client'; +import { PageBody } from '@/components/common/PageBody'; +import { LinkProvider } from '@/app/(main)/links/LinkProvider'; +import { LinkHeader } from '@/app/(main)/links/[linkId]/LinkHeader'; +import { Panel } from '@/components/common/Panel'; +import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; +import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; +import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; +import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels'; + +export function LinkPage({ linkId }: { linkId: string }) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkPanels.tsx b/src/app/(main)/links/[linkId]/LinkPanels.tsx new file mode 100644 index 00000000..d6f515f5 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkPanels.tsx @@ -0,0 +1,83 @@ +import { Grid, Tabs, Tab, TabList, TabPanel, Heading } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { WorldMap } from '@/components/metrics/WorldMap'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function LinkPanels({ linkId }: { linkId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId: linkId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + + + + {formatMessage(labels.sources)} + + + {formatMessage(labels.referrers)} + {formatMessage(labels.channels)} + + + + + + + + + + + {formatMessage(labels.environment)} + + + {formatMessage(labels.browsers)} + {formatMessage(labels.os)} + {formatMessage(labels.devices)} + + + + + + + + + + + + + + + + + + + {formatMessage(labels.location)} + + + {formatMessage(labels.countries)} + {formatMessage(labels.regions)} + {formatMessage(labels.cities)} + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/links/[linkId]/page.tsx b/src/app/(main)/links/[linkId]/page.tsx new file mode 100644 index 00000000..ae3b2c9a --- /dev/null +++ b/src/app/(main)/links/[linkId]/page.tsx @@ -0,0 +1,12 @@ +import { LinkPage } from './LinkPage'; +import { Metadata } from 'next'; + +export default async function ({ params }: { params: Promise<{ linkId: string }> }) { + const { linkId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Link', +}; diff --git a/src/app/(main)/pixels/PixelEditForm.tsx b/src/app/(main)/pixels/PixelEditForm.tsx index 8026c76a..6024a745 100644 --- a/src/app/(main)/pixels/PixelEditForm.tsx +++ b/src/app/(main)/pixels/PixelEditForm.tsx @@ -31,8 +31,8 @@ export function PixelEditForm({ onSave?: () => void; onClose?: () => void; }) { - const { formatMessage, labels } = useMessages(); - const { mutate, error, isPending, touch } = useUpdateQuery( + const { formatMessage, labels, messages } = useMessages(); + const { mutate, error, isPending, touch, toast } = useUpdateQuery( pixelId ? `/pixels/${pixelId}` : '/pixels', { id: pixelId, @@ -47,6 +47,7 @@ export function PixelEditForm({ const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { + toast(formatMessage(messages.saved)); touch('pixels'); onSave?.(); onClose?.(); diff --git a/src/app/(main)/pixels/PixelProvider.tsx b/src/app/(main)/pixels/PixelProvider.tsx new file mode 100644 index 00000000..1a5c1d5d --- /dev/null +++ b/src/app/(main)/pixels/PixelProvider.tsx @@ -0,0 +1,20 @@ +'use client'; +import { createContext, ReactNode } from 'react'; +import { usePixelQuery } from '@/components/hooks'; +import { Loading } from '@umami/react-zen'; + +export const PixelContext = createContext(null); + +export function PixelProvider({ pixelId, children }: { pixelId?: string; children: ReactNode }) { + const { data: pixel, isLoading, isFetching } = usePixelQuery(pixelId); + + if (isFetching && isLoading) { + return ; + } + + if (!pixel) { + return null; + } + + return {children}; +} diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx index cb11554e..dcb5307c 100644 --- a/src/app/(main)/pixels/PixelsTable.tsx +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -1,16 +1,16 @@ +import Link from 'next/link'; import { DataTable, DataColumn, Row } from '@umami/react-zen'; -import { useConfig, useMessages } from '@/components/hooks'; +import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { Empty } from '@/components/common/Empty'; import { DateDistance } from '@/components/common/DateDistance'; import { PixelEditButton } from './PixelEditButton'; import { PixelDeleteButton } from './PixelDeleteButton'; -import { PIXELS_URL } from '@/lib/constants'; import { ExternalLink } from '@/components/common/ExternalLink'; export function PixelsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { pixelsUrl } = useConfig(); - const hostUrl = pixelsUrl || PIXELS_URL; + const { renderUrl } = useNavigation(); + const { getSlugUrl } = useSlug('pixel'); if (data.length === 0) { return ; @@ -18,10 +18,14 @@ export function PixelsTable({ data = [] }) { return ( - + + {({ id, name }: any) => { + return {name}; + }} + {({ slug }: any) => { - const url = `${hostUrl}/${slug}`; + const url = getSlugUrl(slug); return {url}; }} diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx new file mode 100644 index 00000000..c5fee534 --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx @@ -0,0 +1,32 @@ +import { Column, Row } from '@umami/react-zen'; +import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; +import { FilterBar } from '@/components/input/FilterBar'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; +import { ExportButton } from '@/components/input/ExportButton'; + +export function PixelControls({ + pixelId: websiteId, + allowFilter = true, + allowDateFilter = true, + allowMonthFilter, + allowDownload = false, +}: { + pixelId: string; + allowFilter?: boolean; + allowDateFilter?: boolean; + allowMonthFilter?: boolean; + allowDownload?: boolean; +}) { + return ( + + + {allowFilter ? :
} + {allowDateFilter && } + {allowDownload && } + {allowMonthFilter && } + + {allowFilter && } + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx new file mode 100644 index 00000000..aba7c71c --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx @@ -0,0 +1,22 @@ +import { usePixel, useMessages, useSlug } from '@/components/hooks'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Icon, Text } from '@umami/react-zen'; +import { ExternalLink, Pixel } from '@/components/icons'; +import { LinkButton } from '@/components/common/LinkButton'; + +export function PixelHeader() { + const { formatMessage, labels } = useMessages(); + const { getSlugUrl } = useSlug('pixel'); + const pixel = usePixel(); + + return ( + }> + + + + + {formatMessage(labels.view)} + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx new file mode 100644 index 00000000..5b01ef84 --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx @@ -0,0 +1,71 @@ +import { useDateRange, useMessages } from '@/components/hooks'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; +import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; + +export function PixelMetricsBar({ + pixelId, +}: { + pixelId: string; + showChange?: boolean; + compareMode?: boolean; +}) { + const { dateRange } = useDateRange(pixelId); + const { formatMessage, labels } = useMessages(); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId); + const isAllTime = dateRange.value === 'all'; + + const { pageviews, visitors, visits, comparison } = data || {}; + + const metrics = data + ? [ + { + value: visitors, + label: formatMessage(labels.visitors), + change: visitors - comparison.visitors, + formatValue: formatLongNumber, + }, + { + value: visits, + label: formatMessage(labels.visits), + change: visits - comparison.visits, + formatValue: formatLongNumber, + }, + { + value: pageviews, + label: formatMessage(labels.views), + change: pageviews - comparison.pageviews, + formatValue: formatLongNumber, + }, + ] + : null; + + return ( + + + {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => { + return ( + + ); + })} + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx new file mode 100644 index 00000000..6a55a6eb --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx @@ -0,0 +1,25 @@ +'use client'; +import { PageBody } from '@/components/common/PageBody'; +import { PixelProvider } from '@/app/(main)/pixels/PixelProvider'; +import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader'; +import { Panel } from '@/components/common/Panel'; +import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; +import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; +import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls'; +import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels'; + +export function PixelPage({ pixelId }: { pixelId: string }) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx new file mode 100644 index 00000000..a5ea248e --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx @@ -0,0 +1,83 @@ +import { Grid, Tabs, Tab, TabList, TabPanel, Heading } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { WorldMap } from '@/components/metrics/WorldMap'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function PixelPanels({ pixelId }: { pixelId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId: pixelId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + + + + {formatMessage(labels.sources)} + + + {formatMessage(labels.referrers)} + {formatMessage(labels.channels)} + + + + + + + + + + + {formatMessage(labels.environment)} + + + {formatMessage(labels.browsers)} + {formatMessage(labels.os)} + {formatMessage(labels.devices)} + + + + + + + + + + + + + + + + + + + {formatMessage(labels.location)} + + + {formatMessage(labels.countries)} + {formatMessage(labels.regions)} + {formatMessage(labels.cities)} + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/page.tsx b/src/app/(main)/pixels/[pixelId]/page.tsx new file mode 100644 index 00000000..1cb72c1d --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/page.tsx @@ -0,0 +1,12 @@ +import { PixelPage } from './PixelPage'; +import { Metadata } from 'next'; + +export default async function ({ params }: { params: Promise<{ pixelId: string }> }) { + const { pixelId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Pixel', +}; diff --git a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx index 3736299d..8ad2b694 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { TeamProvider } from '@/app/(main)/teams/TeamProvider'; import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings'; export function TeamSettingsPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 59df2f3c..e5140cb2 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,6 +1,6 @@ 'use client'; import { Column } from '@umami/react-zen'; -import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings'; import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader'; import { Panel } from '@/components/common/Panel'; diff --git a/src/app/(main)/teams/[teamId]/TeamProvider.tsx b/src/app/(main)/teams/TeamProvider.tsx similarity index 100% rename from src/app/(main)/teams/[teamId]/TeamProvider.tsx rename to src/app/(main)/teams/TeamProvider.tsx diff --git a/src/app/(main)/teams/[teamId]/TeamEditForm.tsx b/src/app/(main)/teams/[teamId]/TeamEditForm.tsx index 9c053ac8..a7b3ff25 100644 --- a/src/app/(main)/teams/[teamId]/TeamEditForm.tsx +++ b/src/app/(main)/teams/[teamId]/TeamEditForm.tsx @@ -8,9 +8,7 @@ import { useToast, } from '@umami/react-zen'; import { getRandomChars } from '@/lib/crypto'; -import { useContext } from 'react'; -import { useApi, useMessages, useModified } from '@/components/hooks'; -import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { useApi, useMessages, useModified, useTeam } from '@/components/hooks'; const generateId = () => `team_${getRandomChars(16)}`; @@ -23,7 +21,7 @@ export function TeamEditForm({ allowEdit?: boolean; onSave?: () => void; }) { - const team = useContext(TeamContext); + const team = useTeam(); const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); const { toast } = useToast(); diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx index acf1a936..fe9647ea 100644 --- a/src/app/(main)/teams/[teamId]/TeamSettings.tsx +++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx @@ -1,7 +1,6 @@ -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen'; -import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; -import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks'; +import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; import { Users } from '@/components/icons'; @@ -14,7 +13,7 @@ import { PageHeader } from '@/components/common/PageHeader'; import { Panel } from '@/components/common/Panel'; export function TeamSettings({ teamId }: { teamId: string }) { - const team = useContext(TeamContext); + const team = useTeam(); const { formatMessage, labels } = useMessages(); const { user } = useLoginQuery(); const { query, pathname } = useNavigation(); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx b/src/app/(main)/websites/WebsiteProvider.tsx similarity index 100% rename from src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx rename to src/app/(main)/websites/WebsiteProvider.tsx diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx index 084b73b7..a5999a7c 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx @@ -11,7 +11,7 @@ export function UTMPage({ websiteId }: { websiteId: string }) { return ( - + ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 743b4f62..b72f00a0 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -9,7 +9,7 @@ export function WebsiteChart({ compareMode, }: { websiteId: string; - compareMode?: string; + compareMode?: boolean; }) { const { dateRange, dateCompare } = useDateRange(websiteId); const { startDate, endDate, unit, value } = dateRange; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx deleted file mode 100644 index 90386a3a..00000000 --- a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { Grid, Heading, Column, Row, NavMenu, NavMenuItem, Text } from '@umami/react-zen'; -import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { ChangeLabel } from '@/components/metrics/ChangeLabel'; -import { CitiesTable } from '@/components/metrics/CitiesTable'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; -import { EventsTable } from '@/components/metrics/EventsTable'; -import { LanguagesTable } from '@/components/metrics/LanguagesTable'; -import { MetricsTable } from '@/components/metrics/MetricsTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { PagesTable } from '@/components/metrics/PagesTable'; -import { QueryParametersTable } from '@/components/metrics/QueryParametersTable'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { RegionsTable } from '@/components/metrics/RegionsTable'; -import { ScreenTable } from '@/components/metrics/ScreenTable'; -import { TagsTable } from '@/components/metrics/TagsTable'; -import { getCompareDate } from '@/lib/date'; -import { formatNumber } from '@/lib/format'; -import { useState } from 'react'; -import { Panel } from '@/components/common/Panel'; -import { DateDisplay } from '@/components/common/DateDisplay'; - -const views = { - path: PagesTable, - title: PagesTable, - referrer: ReferrersTable, - browser: BrowsersTable, - os: OSTable, - device: DevicesTable, - screen: ScreenTable, - country: CountriesTable, - region: RegionsTable, - city: CitiesTable, - language: LanguagesTable, - event: EventsTable, - query: QueryParametersTable, - tag: TagsTable, -}; - -export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { - const [data] = useState([]); - const { dateRange, dateCompare } = useDateRange(websiteId); - const { formatMessage, labels } = useMessages(); - const { - updateParams, - query: { view }, - } = useNavigation(); - const Component: typeof MetricsTable = views[view || 'path'] || (() => null); - - const items = [ - { - id: 'path', - label: formatMessage(labels.pages), - path: updateParams({ view: 'path' }), - }, - { - id: 'referrer', - label: formatMessage(labels.referrers), - path: updateParams({ view: 'referrer' }), - }, - { - id: 'browser', - label: formatMessage(labels.browsers), - path: updateParams({ view: 'browser' }), - }, - { - id: 'os', - label: formatMessage(labels.os), - path: updateParams({ view: 'os' }), - }, - { - id: 'device', - label: formatMessage(labels.devices), - path: updateParams({ view: 'device' }), - }, - { - id: 'country', - label: formatMessage(labels.countries), - path: updateParams({ view: 'country' }), - }, - { - id: 'region', - label: formatMessage(labels.regions), - path: updateParams({ view: 'region' }), - }, - { - id: 'city', - label: formatMessage(labels.cities), - path: updateParams({ view: 'city' }), - }, - { - id: 'language', - label: formatMessage(labels.languages), - path: updateParams({ view: 'language' }), - }, - { - id: 'screen', - label: formatMessage(labels.screens), - path: updateParams({ view: 'screen' }), - }, - { - id: 'event', - label: formatMessage(labels.events), - path: updateParams({ view: 'event' }), - }, - { - id: 'query', - label: formatMessage(labels.queryParameters), - path: updateParams({ view: 'query' }), - }, - { - id: 'hostname', - label: formatMessage(labels.hostname), - path: updateParams({ view: 'hostname' }), - }, - { - id: 'tag', - label: formatMessage(labels.tags), - path: updateParams({ view: 'tag' }), - }, - ]; - - const renderChange = ({ x, y }) => { - const prev = data.find(d => d.x === x)?.y; - const value = y - prev; - const change = Math.abs(((y - prev) / prev) * 100); - - return ( - !isNaN(change) && ( - - {formatNumber(change)}% - - ) - ); - }; - - const { startDate, endDate } = getCompareDate( - dateCompare, - dateRange.startDate, - dateRange.endDate, - ); - - const params = { - startAt: startDate.getTime(), - endAt: endDate.getTime(), - }; - - return ( - - - - {items.map(({ id, label }) => { - return ( - - {label} - - ); - })} - - - - {formatMessage(labels.previous)} - - - - - - - {formatMessage(labels.current)} - - - - - - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index f0471ed6..97be1821 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -10,15 +10,15 @@ export function WebsiteControls({ allowFilter = true, allowDateFilter = true, allowMonthFilter, - allowCompare, allowDownload = false, + allowCompare = false, }: { websiteId: string; allowFilter?: boolean; - allowCompare?: boolean; allowDateFilter?: boolean; allowMonthFilter?: boolean; allowDownload?: boolean; + allowCompare?: boolean; }) { return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx index ad2f8653..ed0fb806 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx @@ -1,43 +1,7 @@ -import { Grid, Column, NavMenu, NavMenuItem } from '@umami/react-zen'; +import { Grid, Column } from '@umami/react-zen'; import { useMessages, useNavigation } from '@/components/hooks'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { CitiesTable } from '@/components/metrics/CitiesTable'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; -import { EventsTable } from '@/components/metrics/EventsTable'; -import { HostnamesTable } from '@/components/metrics/HostnamesTable'; -import { LanguagesTable } from '@/components/metrics/LanguagesTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { PagesTable } from '@/components/metrics/PagesTable'; -import { QueryParametersTable } from '@/components/metrics/QueryParametersTable'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { RegionsTable } from '@/components/metrics/RegionsTable'; -import { ScreenTable } from '@/components/metrics/ScreenTable'; -import { TagsTable } from '@/components/metrics/TagsTable'; -import { ChannelsTable } from '@/components/metrics/ChannelsTable'; -import Link from 'next/link'; - -const views = { - path: PagesTable, - entry: PagesTable, - exit: PagesTable, - title: PagesTable, - referrer: ReferrersTable, - grouped: ReferrersTable, - hostname: HostnamesTable, - browser: BrowsersTable, - os: OSTable, - device: DevicesTable, - screen: ScreenTable, - country: CountriesTable, - region: RegionsTable, - city: CitiesTable, - language: LanguagesTable, - event: EventsTable, - query: QueryParametersTable, - tag: TagsTable, - channel: ChannelsTable, -}; +import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable'; +import { SideMenu } from '@/components/common/SideMenu'; export function WebsiteExpandedView({ websiteId, @@ -54,106 +18,137 @@ export function WebsiteExpandedView({ const items = [ { - id: 'path', label: formatMessage(labels.pages), - path: updateParams({ view: 'path' }), + items: [ + { + id: 'path', + label: formatMessage(labels.path), + path: updateParams({ view: 'path' }), + }, + { + id: 'entry', + label: formatMessage(labels.entry), + path: updateParams({ view: 'entry' }), + }, + { + id: 'exit', + label: formatMessage(labels.exit), + path: updateParams({ view: 'exit' }), + }, + { + id: 'title', + label: formatMessage(labels.title), + path: updateParams({ view: 'title' }), + }, + { + id: 'query', + label: formatMessage(labels.query), + path: updateParams({ view: 'query' }), + }, + ], }, { - id: 'referrer', - label: formatMessage(labels.referrers), - path: updateParams({ view: 'referrer' }), + label: formatMessage(labels.sources), + items: [ + { + id: 'referrer', + label: formatMessage(labels.referrers), + path: updateParams({ view: 'referrer' }), + }, + { + id: 'channel', + label: formatMessage(labels.channels), + path: updateParams({ view: 'channel' }), + }, + { + id: 'domain', + label: formatMessage(labels.domain), + path: updateParams({ view: 'domain' }), + }, + ], }, { - id: 'channel', - label: formatMessage(labels.channels), - path: updateParams({ view: 'channel' }), + label: formatMessage(labels.location), + items: [ + { + id: 'country', + label: formatMessage(labels.countries), + path: updateParams({ view: 'country' }), + }, + { + id: 'region', + label: formatMessage(labels.regions), + path: updateParams({ view: 'region' }), + }, + { + id: 'city', + label: formatMessage(labels.cities), + path: updateParams({ view: 'city' }), + }, + ], }, { - id: 'browser', - label: formatMessage(labels.browsers), - path: updateParams({ view: 'browser' }), + label: formatMessage(labels.environment), + items: [ + { + id: 'browser', + label: formatMessage(labels.browsers), + path: updateParams({ view: 'browser' }), + }, + { + id: 'os', + label: formatMessage(labels.os), + path: updateParams({ view: 'os' }), + }, + { + id: 'device', + label: formatMessage(labels.devices), + path: updateParams({ view: 'device' }), + }, + { + id: 'language', + label: formatMessage(labels.languages), + path: updateParams({ view: 'language' }), + }, + { + id: 'screen', + label: formatMessage(labels.screens), + path: updateParams({ view: 'screen' }), + }, + ], }, { - id: 'os', - label: formatMessage(labels.os), - path: updateParams({ view: 'os' }), - }, - { - id: 'device', - label: formatMessage(labels.devices), - path: updateParams({ view: 'device' }), - }, - { - id: 'country', - label: formatMessage(labels.countries), - path: updateParams({ view: 'country' }), - }, - { - id: 'region', - label: formatMessage(labels.regions), - path: updateParams({ view: 'region' }), - }, - { - id: 'city', - label: formatMessage(labels.cities), - path: updateParams({ view: 'city' }), - }, - { - id: 'language', - label: formatMessage(labels.languages), - path: updateParams({ view: 'language' }), - }, - { - id: 'screen', - label: formatMessage(labels.screens), - path: updateParams({ view: 'screen' }), - }, - { - id: 'event', - label: formatMessage(labels.events), - path: updateParams({ view: 'event' }), - }, - { - id: 'query', - label: formatMessage(labels.queryParameters), - path: updateParams({ view: 'query' }), - }, - { - id: 'hostname', - label: formatMessage(labels.hostname), - path: updateParams({ view: 'hostname' }), - }, - { - id: 'tag', - label: formatMessage(labels.tags), - path: updateParams({ view: 'tag' }), + label: formatMessage(labels.other), + items: [ + { + id: 'event', + label: formatMessage(labels.events), + path: updateParams({ view: 'event' }), + }, + { + id: 'hostname', + label: formatMessage(labels.hostname), + path: updateParams({ view: 'hostname' }), + }, + { + id: 'tag', + label: formatMessage(labels.tags), + path: updateParams({ view: 'tag' }), + }, + ], }, ]; - const DetailsComponent = views[view] || (() => null); - return ( - - - {items.map(({ id, label, path }) => { - return ( - - {label} - - ); - })} - + + - diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index 357d9a5c..0aa57a2c 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -1,11 +1,10 @@ import { Button, Icon, Text, Row, DialogTrigger, Dialog, Modal } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; -import { useWebsite } from '@/components/hooks/useWebsite'; import { Share, Edit } from '@/components/icons'; import { Favicon } from '@/components/common/Favicon'; import { ActiveUsers } from '@/components/metrics/ActiveUsers'; import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm'; -import { useMessages, useNavigation } from '@/components/hooks'; +import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; import { LinkButton } from '@/components/common/LinkButton'; export function WebsiteHeader() { diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index 152c2a8f..cf75e86a 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -1,10 +1,10 @@ 'use client'; import { ReactNode } from 'react'; import { Column, Grid } from '@umami/react-zen'; -import { WebsiteProvider } from './WebsiteProvider'; +import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { PageBody } from '@/components/common/PageBody'; import { WebsiteHeader } from './WebsiteHeader'; -import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; +import { WebsiteNav } from './WebsiteNav'; export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index e2a6bc36..7637a452 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -13,6 +13,7 @@ import { Network, ChartPie, UserPlus, + Compare, } from '@/components/icons'; import { useMessages, useNavigation } from '@/components/hooks'; import { SideMenu } from '@/components/common/SideMenu'; @@ -22,7 +23,8 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { pathname, renderUrl, teamId } = useNavigation(); - const renderPath = (path: string) => renderUrl(`/websites/${websiteId}${path}`); + const renderPath = (path: string) => + renderUrl(`/websites/${websiteId}${path}`, { event: undefined }); const items = [ { @@ -52,6 +54,12 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { icon: , path: renderPath('/realtime'), }, + { + id: 'compare', + label: formatMessage(labels.compare), + icon: , + path: renderPath('/compare'), + }, { id: 'breakdown', label: formatMessage(labels.breakdown), @@ -132,8 +140,8 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { ]; const selectedKey = - items.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path))?.id || - 'overview'; + items.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path.split('?')[0])) + ?.id || 'overview'; return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx similarity index 83% rename from src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx rename to src/app/(main)/websites/[websiteId]/WebsitePage.tsx index 33bca5ae..9b08427d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx @@ -5,10 +5,10 @@ import { Panel } from '@/components/common/Panel'; import { WebsiteChart } from './WebsiteChart'; import { WebsiteExpandedView } from './WebsiteExpandedView'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; -import { WebsiteTableView } from './WebsiteTableView'; +import { WebsitePanels } from './WebsitePanels'; import { WebsiteControls } from './WebsiteControls'; -export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) { +export function WebsitePage({ websiteId }: { websiteId: string }) { const { router, query: { view, compare }, @@ -27,12 +27,12 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) { return ( - + - + - + {({ close }) => { diff --git a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx new file mode 100644 index 00000000..b7358649 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx @@ -0,0 +1,114 @@ +import { Grid, Tabs, Tab, TabList, TabPanel, Heading, Row } from '@umami/react-zen'; +import { GridRow } from '@/components/common/GridRow'; +import { Panel } from '@/components/common/Panel'; +import { WorldMap } from '@/components/metrics/WorldMap'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic'; +import { useMessages } from '@/components/hooks'; + +export function WebsitePanels({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + const tableProps = { + websiteId, + limit: 10, + allowDownload: false, + showMore: true, + metric: formatMessage(labels.visitors), + }; + const rowProps = { minHeight: 570 }; + + return ( + + + + {formatMessage(labels.pages)} + + + {formatMessage(labels.path)} + {formatMessage(labels.entry)} + {formatMessage(labels.exit)} + + + + + + + + + + + + + + {formatMessage(labels.sources)} + + + {formatMessage(labels.referrers)} + {formatMessage(labels.channels)} + + + + + + + + + + + + + + + + {formatMessage(labels.location)} + + + {formatMessage(labels.countries)} + {formatMessage(labels.regions)} + {formatMessage(labels.cities)} + + + + + + + + + + + + + + + + {formatMessage(labels.environment)} + + + {formatMessage(labels.browsers)} + {formatMessage(labels.os)} + {formatMessage(labels.devices)} + + + + + + + + + + + + + + {formatMessage(labels.traffic)} + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx deleted file mode 100644 index 93567cc1..00000000 --- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Grid } from '@umami/react-zen'; -import { GridRow } from '@/components/common/GridRow'; -import { Panel } from '@/components/common/Panel'; -import { PagesTable } from '@/components/metrics/PagesTable'; -import { ReferrersTable } from '@/components/metrics/ReferrersTable'; -import { BrowsersTable } from '@/components/metrics/BrowsersTable'; -import { OSTable } from '@/components/metrics/OSTable'; -import { DevicesTable } from '@/components/metrics/DevicesTable'; -import { WorldMap } from '@/components/metrics/WorldMap'; -import { CountriesTable } from '@/components/metrics/CountriesTable'; - -export function WebsiteTableView({ websiteId }: { websiteId: string }) { - const props = { websiteId, limit: 10, allowDownload: false }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx index c569b73c..e69b80df 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx @@ -1,6 +1,5 @@ import { Tabs, TabList, Tab, Icon, Text, Row } from '@umami/react-zen'; -import { useWebsite } from '@/components/hooks/useWebsite'; -import { useMessages, useNavigation } from '@/components/hooks'; +import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; import { Clock, Eye, Lightning, User, ChartPie } from '@/components/icons'; export function WebsiteTabs() { diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx index 578c4fc6..200449fc 100644 --- a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx @@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance'; import { filtersObjectToArray } from '@/lib/params'; import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton'; import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton'; +import Link from 'next/link'; export function CohortsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { websiteId } = useNavigation(); + const { websiteId, renderUrl } = useNavigation(); if (data.length === 0) { return ; @@ -16,7 +17,11 @@ export function CohortsTable({ data = [] }) { return ( - + + {(row: any) => ( + {row.name} + )} + {(row: any) => } diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx new file mode 100644 index 00000000..4c5b7b93 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx @@ -0,0 +1,20 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { CompareTables } from './CompareTables'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar'; +import { Panel } from '@/components/common/Panel'; +import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; + +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 new file mode 100644 index 00000000..7ba728b5 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import { Grid, Heading, Column, Row, Select, ListItem } from '@umami/react-zen'; +import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; +import { Panel } from '@/components/common/Panel'; +import { DateDisplay } from '@/components/common/DateDisplay'; +import { ChangeLabel } from '@/components/metrics/ChangeLabel'; +import { getCompareDate } from '@/lib/date'; +import { formatNumber } from '@/lib/format'; + +export function CompareTables({ websiteId }: { websiteId: string }) { + const [data, setData] = useState([]); + const { dateRange, dateCompare } = useDateRange(websiteId); + const { formatMessage, labels } = useMessages(); + const { + router, + updateParams, + query: { view = 'path' }, + } = useNavigation(); + + const items = [ + { + id: 'path', + label: formatMessage(labels.path), + path: updateParams({ view: 'path' }), + }, + { + id: 'referrer', + label: formatMessage(labels.referrers), + path: updateParams({ view: 'referrer' }), + }, + { + id: 'browser', + label: formatMessage(labels.browsers), + path: updateParams({ view: 'browser' }), + }, + { + id: 'os', + label: formatMessage(labels.os), + path: updateParams({ view: 'os' }), + }, + { + id: 'device', + label: formatMessage(labels.devices), + path: updateParams({ view: 'device' }), + }, + { + id: 'country', + label: formatMessage(labels.countries), + path: updateParams({ view: 'country' }), + }, + { + id: 'region', + label: formatMessage(labels.regions), + path: updateParams({ view: 'region' }), + }, + { + id: 'city', + label: formatMessage(labels.cities), + path: updateParams({ view: 'city' }), + }, + { + id: 'language', + label: formatMessage(labels.languages), + path: updateParams({ view: 'language' }), + }, + { + id: 'screen', + label: formatMessage(labels.screens), + path: updateParams({ view: 'screen' }), + }, + { + id: 'event', + label: formatMessage(labels.events), + path: updateParams({ view: 'event' }), + }, + { + id: 'hostname', + label: formatMessage(labels.hostname), + path: updateParams({ view: 'hostname' }), + }, + { + id: 'tag', + label: formatMessage(labels.tags), + path: updateParams({ view: 'tag' }), + }, + ]; + + const renderChange = props => { + const { label: x, count: y } = props; + const prev = data.find(d => d.x === x)?.y; + const value = y - prev; + const change = Math.abs(((y - prev) / prev) * 100); + + return ( + !isNaN(change) && ( + + {formatNumber(change)}% + + ) + ); + }; + + const handleChange = id => { + router.push(updateParams({ view: id })); + }; + + const { startDate, endDate } = getCompareDate( + dateCompare, + dateRange.startDate, + dateRange.endDate, + ); + + const params = { + startAt: startDate.getTime(), + endAt: endDate.getTime(), + }; + + return ( + <> + + + + + + + + {formatMessage(labels.previous)} + + + + + + + {formatMessage(labels.current)} + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/compare/page.tsx b/src/app/(main)/websites/[websiteId]/compare/page.tsx new file mode 100644 index 00000000..2b2cf5b1 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { ComparePage } from './ComparePage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Compare', +}; diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 26d2f265..60985a4c 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -100,9 +100,9 @@ const EventValues = ({ websiteId, eventName, propertyName }) => { const tableData = useMemo(() => { if (!propertyName || !values || propertySum === 0) return []; return values.map(({ value, total }) => ({ - x: value, - y: total, - z: 100 * (total / propertySum), + label: value, + count: total, + percent: 100 * (total / propertySum), })); }, [propertyName, values, propertySum]); diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 14a90c9f..48e8b015 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -1,6 +1,6 @@ 'use client'; import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen'; -import { EventsTable } from '@/components/metrics/EventsTable'; +import { MetricsTable } from '@/components/metrics/MetricsTable'; import { useState, Key } from 'react'; import { EventsDataTable } from './EventsDataTable'; import { Panel } from '@/components/common/Panel'; @@ -13,14 +13,9 @@ import { getItem, setItem } from '@/lib/storage'; const KEY_NAME = 'umami.events.tab'; export function EventsPage({ websiteId }) { - const [label, setLabel] = useState(null); const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart'); const { formatMessage, labels } = useMessages(); - const handleLabelClick = (value: string) => { - setLabel(value !== label ? value : ''); - }; - const handleSelect = (value: Key) => { setItem(KEY_NAME, value); setTab(value); @@ -42,14 +37,13 @@ export function EventsPage({ websiteId }) { - + - diff --git a/src/app/(main)/websites/[websiteId]/page.tsx b/src/app/(main)/websites/[websiteId]/page.tsx index 286a4612..9755e6d0 100644 --- a/src/app/(main)/websites/[websiteId]/page.tsx +++ b/src/app/(main)/websites/[websiteId]/page.tsx @@ -1,10 +1,10 @@ -import { WebsiteDetailsPage } from './WebsiteDetailsPage'; +import { WebsitePage } from './WebsitePage'; import { Metadata } from 'next'; -export default async function WebsitePage({ params }: { params: Promise<{ websiteId: string }> }) { +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { const { websiteId } = await params; - return ; + return ; } export const metadata: Metadata = { diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index a706242a..c7bd1347 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -1,15 +1,19 @@ import { useFormat } from '@/components//hooks/useFormat'; import { Empty } from '@/components/common/Empty'; import { FilterButtons } from '@/components/input/FilterButtons'; -import { useCountryNames, useLocale, useMessages, useTimezone } from '@/components/hooks'; +import { + useCountryNames, + useLocale, + useMessages, + useTimezone, + useWebsite, +} from '@/components/hooks'; import { Eye, Visitor, Bolt } from '@/components/icons'; import { BROWSERS, OS_NAMES } from '@/lib/constants'; import { stringToColor } from '@/lib/format'; -import { RealtimeData } from '@/lib/types'; -import { useContext, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { Icon, SearchField, StatusLight, Text } from '@umami/react-zen'; import { FixedSizeList } from 'react-window'; -import { WebsiteContext } from '../WebsiteProvider'; import styles from './RealtimeLog.module.css'; const TYPE_ALL = 'all'; @@ -23,8 +27,8 @@ const icons = { [TYPE_EVENT]: , }; -export function RealtimeLog({ data }: { data: RealtimeData }) { - const website = useContext(WebsiteContext); +export function RealtimeLog({ data }: { data: any }) { + const website = useWebsite(); const [search, setSearch] = useState(''); const { formatMessage, labels, messages } = useMessages(); const { formatValue } = useFormat(); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx index 15e53a6b..a6e90763 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx @@ -1,18 +1,16 @@ -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { Row } from '@umami/react-zen'; import thenby from 'thenby'; import { percentFilter } from '@/lib/filters'; import { ListTable } from '@/components/metrics/ListTable'; -import { useMessages } from '@/components/hooks'; -import { RealtimeData } from '@/lib/types'; -import { WebsiteContext } from '../WebsiteProvider'; +import { useMessages, useWebsite } from '@/components/hooks'; import { FilterButtons } from '@/components/input/FilterButtons'; const FILTER_REFERRERS = 'filter-referrers'; const FILTER_PAGES = 'filter-pages'; -export function RealtimeUrls({ data }: { data: RealtimeData }) { - const website = useContext(WebsiteContext); +export function RealtimeUrls({ data }: { data: any }) { + const website = useWebsite(); const { formatMessage, labels } = useMessages(); const { referrers, urls } = data || {}; const [filter, setFilter] = useState(FILTER_REFERRERS); diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx index c93b1543..7791ef82 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx @@ -11,8 +11,9 @@ import { import { subMonths, endOfDay } from 'date-fns'; import { FieldFilters } from '@/components/input/FieldFilters'; import { useState } from 'react'; -import { useApi, useMessages, useModified, useWebsiteSegmentQuery } from '@/components/hooks'; +import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks'; import { filtersArrayToObject } from '@/lib/params'; +import { messages } from '@/components/messages'; export function SegmentEditForm({ segmentId, @@ -32,24 +33,23 @@ export function SegmentEditForm({ const { data } = useWebsiteSegmentQuery(websiteId, segmentId); const { formatMessage, labels } = useMessages(); const [currentFilters, setCurrentFilters] = useState(filters); - const { touch } = useModified(); const startDate = subMonths(endOfDay(new Date()), 6); const endDate = endOfDay(new Date()); - const { post, useMutation } = useApi(); - const { mutate, error, isPending } = useMutation({ - mutationFn: (data: any) => - post(`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, { - ...data, - type: 'segment', - }), - }); + const { mutate, error, isPending, touch, toast } = useUpdateQuery( + `/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, + { + ...data, + type: 'segment', + }, + ); const handleSubmit = async (data: any) => { mutate( { ...data, parameters: filtersArrayToObject(currentFilters) }, { onSuccess: async () => { + toast(formatMessage(messages.saved)); touch('segments'); onSave?.(); onClose?.(); diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx index fe8317ce..6f75aa9e 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx @@ -5,10 +5,11 @@ import { DateDistance } from '@/components/common/DateDistance'; import { filtersObjectToArray } from '@/lib/params'; import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton'; import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton'; +import Link from 'next/link'; export function SegmentsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { websiteId } = useNavigation(); + const { websiteId, renderUrl } = useNavigation(); if (data.length === 0) { return ; @@ -16,7 +17,11 @@ export function SegmentsTable({ data = [] }) { return ( - + + {(row: any) => ( + {row.name} + )} + {(row: any) => } diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index d60713d9..ea4b5f03 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -71,9 +71,9 @@ const SessionValues = ({ websiteId, propertyName }) => { const tableData = useMemo(() => { if (!propertyName || !data || propertySum === 0) return []; return data.map(({ value, total }) => ({ - x: value, - y: total, - z: 100 * (total / propertySum), + label: value, + count: total, + percent: 100 * (total / propertySum), })); }, [propertyName, data, propertySum]); diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx index cb7cb0d1..5beeaf70 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { FormSubmitButton, Form, @@ -7,12 +6,11 @@ import { TextField, useToast, } from '@umami/react-zen'; -import { useApi, useMessages, useModified } from '@/components/hooks'; +import { useApi, useMessages, useModified, useWebsite } from '@/components/hooks'; import { DOMAIN_REGEX } from '@/lib/constants'; -import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { - const website = useContext(WebsiteContext); + const website = useWebsite(); const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); const { toast } = useToast(); diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx index baf59452..88a33e77 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx @@ -1,14 +1,12 @@ -import { useContext } from 'react'; import { Tabs, TabList, Tab, TabPanel } from '@umami/react-zen'; -import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; -import { useMessages } from '@/components/hooks'; +import { useMessages, useWebsite } from '@/components/hooks'; import { WebsiteShareForm } from './WebsiteShareForm'; import { WebsiteTrackingCode } from './WebsiteTrackingCode'; import { WebsiteData } from './WebsiteData'; import { WebsiteEditForm } from './WebsiteEditForm'; export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) { - const website = useContext(WebsiteContext); + const website = useWebsite(); const { formatMessage, labels } = useMessages(); return ( diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx index ee542f2f..97a0108f 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx @@ -1,10 +1,9 @@ -import { useContext } from 'react'; -import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import { PageHeader } from '@/components/common/PageHeader'; import { Globe } from '@/components/icons'; +import { useWebsite } from '@/components/hooks'; export function WebsiteSettingsHeader() { - const website = useContext(WebsiteContext); + const website = useWebsite(); return } />; } diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx index 1545f84d..d0daefa9 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx @@ -1,4 +1,4 @@ -import { Key, useContext, useState } from 'react'; +import { Key, useState } from 'react'; import { Button, Form, @@ -10,8 +10,13 @@ import { ListItem, Text, } from '@umami/react-zen'; -import { useApi, useLoginQuery, useMessages, useUserTeamsQuery } from '@/components/hooks'; -import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { + useLoginQuery, + useMessages, + useUpdateQuery, + useUserTeamsQuery, + useWebsite, +} from '@/components/hooks'; import { ROLES } from '@/lib/constants'; export function WebsiteTransferForm({ @@ -24,13 +29,10 @@ export function WebsiteTransferForm({ onClose?: () => void; }) { const { user } = useLoginQuery(); - const website = useContext(WebsiteContext); + const website = useWebsite(); const [teamId, setTeamId] = useState(null); const { formatMessage, labels, messages } = useMessages(); - const { post, useMutation } = useApi(); - const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data), - }); + const { mutate, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`); const { data: teams, isLoading } = useUserTeamsQuery(user.id); const isTeamWebsite = !!website?.teamId; @@ -87,7 +89,11 @@ export function WebsiteTransferForm({ - + {formatMessage(labels.transfer)} diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts index 5494b5a9..fa52e19c 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/validations'; import { pagingParams, timezoneParam } from '@/lib/schema'; -import { getWebsiteSessionsWeekly } from '@/queries'; +import { getWeeklyTraffic } from '@/queries'; export async function GET( request: Request, @@ -30,7 +30,7 @@ export async function GET( const filters = await getQueryFilters(query, websiteId); - const data = await getWebsiteSessionsWeekly(websiteId, filters); + const data = await getWeeklyTraffic(websiteId, filters); return json(data); } diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index c5f734b5..ae2be609 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,6 +1,6 @@ 'use client'; -import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; -import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage'; +import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; +import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage'; import { useShareTokenQuery } from '@/components/hooks'; import { PageBody } from '@/components/common/PageBody'; import { Header } from './Header'; @@ -17,7 +17,7 @@ export function SharePage({ shareId }) {
- +