Compare commits

...

2 commits

Author SHA1 Message Date
Mike Cao
980e4e6b41 Fixed funnel report saving invalid data.
Some checks are pending
Node.js CI / build (postgresql, 18.18) (push) Waiting to run
2025-09-22 22:03:26 -07:00
Mike Cao
bf16ade184 Updated icons. 2025-09-22 20:11:31 -07:00
30 changed files with 183 additions and 178 deletions

View file

@ -82,7 +82,7 @@
"@react-spring/web": "^10.0.1", "@react-spring/web": "^10.0.1",
"@svgr/cli": "^8.1.0", "@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.85.5", "@tanstack/react-query": "^5.85.5",
"@umami/react-zen": "^0.181.0", "@umami/react-zen": "^0.184.0",
"@umami/redis-client": "^0.29.0", "@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"chalk": "^5.6.0", "chalk": "^5.6.0",

58
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5 specifier: ^5.85.5
version: 5.85.5(react@19.1.1) version: 5.85.5(react@19.1.1)
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.181.0 specifier: ^0.184.0
version: 0.181.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) version: 0.184.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
'@umami/redis-client': '@umami/redis-client':
specifier: ^0.29.0 specifier: ^0.29.0
version: 0.29.0 version: 0.29.0
@ -1061,8 +1061,8 @@ packages:
'@fontsource/inter@5.2.6': '@fontsource/inter@5.2.6':
resolution: {integrity: sha512-CZs9S1CrjD0jPwsNy9W6j0BhsmRSQrgwlTNkgQXTsAeDRM42LBRLo3eo9gCzfH4GvV7zpyf78Ozfl773826csw==} resolution: {integrity: sha512-CZs9S1CrjD0jPwsNy9W6j0BhsmRSQrgwlTNkgQXTsAeDRM42LBRLo3eo9gCzfH4GvV7zpyf78Ozfl773826csw==}
'@fontsource/jetbrains-mono@5.2.6': '@fontsource/jetbrains-mono@5.2.8':
resolution: {integrity: sha512-nz//dBr99hXZmHp10wgNI00qThWImkzRR5PQjvRM+rpmuHO5rYBJCqPPWufidCvmkkryXx/GOP/lgqsM3R3Org==} resolution: {integrity: sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==}
'@formatjs/cli@4.8.4': '@formatjs/cli@4.8.4':
resolution: {integrity: sha512-zZI8QYVl5CHaT6j9OHjS+0mMnWzopBVH0un4n5b4IhIJRzIKnxwFTkxBp5Ifqj6FntrwzIGqP+D6v8u7MPYsmw==} resolution: {integrity: sha512-zZI8QYVl5CHaT6j9OHjS+0mMnWzopBVH0un4n5b4IhIJRzIKnxwFTkxBp5Ifqj6FntrwzIGqP+D6v8u7MPYsmw==}
@ -1539,8 +1539,8 @@ packages:
'@prisma/get-platform@6.16.0': '@prisma/get-platform@6.16.0':
resolution: {integrity: sha512-eaJOOvAoGslSUTjiQrtE9E0hoBdfL43j8SymOGD6LbdrKRNtIoiy6qiBaEr2fNYD+R/Qns7QOwPhl7SVHJayKA==} resolution: {integrity: sha512-eaJOOvAoGslSUTjiQrtE9E0hoBdfL43j8SymOGD6LbdrKRNtIoiy6qiBaEr2fNYD+R/Qns7QOwPhl7SVHJayKA==}
'@react-aria/autocomplete@3.0.0-rc.1': '@react-aria/autocomplete@3.0.0-rc.2':
resolution: {integrity: sha512-4/+XHkCq9nkC0TNfgPsbuMTu3iwM6Gz4j67rTQRMXrWwCTAqAHJJEmDz/YDt/04Rg+dkGPsauHHMqDxwxZV24Q==} resolution: {integrity: sha512-55KVj5FePFTHk8nWfUUNN8m7rBL+aSRE0CxHI2t8JG3uam3nY7jyuAJy34RBuDEdTsVlMO9Fri/1JragePC2dg==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@ -1569,8 +1569,8 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-aria/collections@3.0.0-rc.6': '@react-aria/collections@3.0.0-rc.7':
resolution: {integrity: sha512-N4AzRqzFJ4BztM1x56ot33smDUUsYQ6pzlbz6m4f8ARSqQYl0a2FsM13PYDtuNI5Dt9KtkL6rK/tLaZlTghLyg==} resolution: {integrity: sha512-JMktVhe+OT6rZVcGdmSWgNj3VBq4Owm3L5LD8iMwJrV6SgPGmyzpguX7JTnz1hnSWO/wD2vrwMWEAlcuL7acBg==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@ -2735,8 +2735,8 @@ packages:
'@prisma/client': ^6.1.0 '@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1 '@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.181.0': '@umami/react-zen@0.184.0':
resolution: {integrity: sha512-Hs5dXPWVOtgHwAksolVTZw8n8EucHq+Xj+++a6RpfzHix3m2sWQXo1qNwd11GasxytqDf6JSxETj109LCUJiSA==} resolution: {integrity: sha512-XfxTiP4ljumflx02ymDMXLnhcJW+mOxxKCPEVEjuDrQfR6VUlbHg0EdH04S4gvCJZJC/WnP6guyO2eabhJL88Q==}
'@umami/redis-client@0.29.0': '@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -6302,14 +6302,14 @@ packages:
rc9@2.1.2: rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
react-aria-components@1.12.1: react-aria-components@1.12.2:
resolution: {integrity: sha512-UTn2y2Pr1QuapXLRoGE/GWHrjcZZSuMf+zhbJjInOHgS+MC4hqXiINufvjQrdjQDzS1llc2aepP9op6+z6QSxA==} resolution: {integrity: sha512-BTA697VWy6Who9cpSbll447kqqpwxYvN6QF3/+AmXO+M+KgUXtPZAaNXu/9Sv2LdshU0zhIea4w27ZOt57UzPQ==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-aria@3.43.1: react-aria@3.43.2:
resolution: {integrity: sha512-/PmZGiw+Ya/YtzXmiLW4ALD4SMuDnbwhMaVh33VCduTl8vVujIUzUTIi5g4C+YHLDs/Z4edsN3aQsPMqFfg1xA==} resolution: {integrity: sha512-CfaXi3S69SeOkpp6pGc1w5FH8OvGPFphiMrO2tNSlqpYIecgk3gKoXjkqaAr6N+O1gasLMfAAF9sxtvS141qWg==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@ -6324,8 +6324,8 @@ packages:
peerDependencies: peerDependencies:
react: '>=16.13.1' react: '>=16.13.1'
react-hook-form@7.62.0: react-hook-form@7.63.0:
resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} resolution: {integrity: sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 react: ^16.8.0 || ^17 || ^18 || ^19
@ -8062,7 +8062,7 @@ snapshots:
'@fontsource/inter@5.2.6': {} '@fontsource/inter@5.2.6': {}
'@fontsource/jetbrains-mono@5.2.6': {} '@fontsource/jetbrains-mono@5.2.8': {}
'@formatjs/cli@4.8.4(ts-jest@29.4.1(@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.9)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)))(typescript@5.9.2))': '@formatjs/cli@4.8.4(ts-jest@29.4.1(@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.9)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)))(typescript@5.9.2))':
dependencies: dependencies:
@ -8650,7 +8650,7 @@ snapshots:
dependencies: dependencies:
'@prisma/debug': 6.16.0 '@prisma/debug': 6.16.0
'@react-aria/autocomplete@3.0.0-rc.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': '@react-aria/autocomplete@3.0.0-rc.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies: dependencies:
'@react-aria/combobox': 3.13.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/combobox': 3.13.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -8723,7 +8723,7 @@ snapshots:
react: 19.1.1 react: 19.1.1
react-dom: 19.1.1(react@19.1.1) react-dom: 19.1.1(react@19.1.1)
'@react-aria/collections@3.0.0-rc.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': '@react-aria/collections@3.0.0-rc.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies: dependencies:
'@react-aria/interactions': 3.25.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/interactions': 3.25.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/ssr': 3.9.10(react@19.1.1) '@react-aria/ssr': 3.9.10(react@19.1.1)
@ -10323,9 +10323,9 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umami/react-zen@0.181.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))': '@umami/react-zen@0.184.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
dependencies: dependencies:
'@fontsource/jetbrains-mono': 5.2.6 '@fontsource/jetbrains-mono': 5.2.8
'@internationalized/date': 3.9.0 '@internationalized/date': 3.9.0
'@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-spring/web': 9.7.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-spring/web': 9.7.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -10335,9 +10335,9 @@ snapshots:
lucide-react: 0.511.0(react@19.1.1) lucide-react: 0.511.0(react@19.1.1)
next: 15.5.3(@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.3(@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: 19.1.1
react-aria-components: 1.12.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-aria-components: 1.12.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-dom: 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-hook-form: 7.63.0(react@19.1.1)
react-icons: 5.5.0(react@19.1.1) react-icons: 5.5.0(react@19.1.1)
thenby: 1.3.4 thenby: 1.3.4
zustand: 5.0.8(@types/react@19.1.12)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) zustand: 5.0.8(@types/react@19.1.12)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
@ -14375,12 +14375,12 @@ snapshots:
defu: 6.1.4 defu: 6.1.4
destr: 2.0.5 destr: 2.0.5
react-aria-components@1.12.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): react-aria-components@1.12.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies: dependencies:
'@internationalized/date': 3.9.0 '@internationalized/date': 3.9.0
'@internationalized/string': 3.2.7 '@internationalized/string': 3.2.7
'@react-aria/autocomplete': 3.0.0-rc.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/autocomplete': 3.0.0-rc.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/collections': 3.0.0-rc.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/collections': 3.0.0-rc.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/dnd': 3.11.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/dnd': 3.11.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/focus': 3.21.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-aria/interactions': 3.25.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/interactions': 3.25.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -14404,12 +14404,12 @@ snapshots:
'@swc/helpers': 0.5.17 '@swc/helpers': 0.5.17
client-only: 0.0.1 client-only: 0.0.1
react: 19.1.1 react: 19.1.1
react-aria: 3.43.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-aria: 3.43.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-dom: 19.1.1(react@19.1.1) react-dom: 19.1.1(react@19.1.1)
react-stately: 3.41.0(react@19.1.1) react-stately: 3.41.0(react@19.1.1)
use-sync-external-store: 1.5.0(react@19.1.1) use-sync-external-store: 1.5.0(react@19.1.1)
react-aria@3.43.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): react-aria@3.43.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies: dependencies:
'@internationalized/string': 3.2.7 '@internationalized/string': 3.2.7
'@react-aria/breadcrumbs': 3.5.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-aria/breadcrumbs': 3.5.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -14466,7 +14466,7 @@ snapshots:
'@babel/runtime': 7.28.3 '@babel/runtime': 7.28.3
react: 19.1.1 react: 19.1.1
react-hook-form@7.62.0(react@19.1.1): react-hook-form@7.63.0(react@19.1.1):
dependencies: dependencies:
react: 19.1.1 react: 19.1.1

View file

@ -8,7 +8,7 @@ import {
SidebarProps, SidebarProps,
ThemeButton, ThemeButton,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { Globe, Link as LinkIcon, Logo, Pixel, PanelLeft } from '@/components/icons'; import { Globe, LinkIcon, LogoSvg, Grid2x2, PanelLeft } from '@/components/icons';
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
import { TeamsButton } from '@/components/input/TeamsButton'; import { TeamsButton } from '@/components/input/TeamsButton';
import { PanelButton } from '@/components/input/PanelButton'; import { PanelButton } from '@/components/input/PanelButton';
@ -39,7 +39,7 @@ export function SideNav(props: SidebarProps) {
id: 'pixels', id: 'pixels',
label: formatMessage(labels.pixels), label: formatMessage(labels.pixels),
path: '/pixels', path: '/pixels',
icon: <Pixel />, icon: <Grid2x2 />,
}, },
]; ];
@ -53,7 +53,7 @@ export function SideNav(props: SidebarProps) {
<SidebarSection onClick={() => setIsCollapsed(false)}> <SidebarSection onClick={() => setIsCollapsed(false)}>
<SidebarHeader <SidebarHeader
label="umami" label="umami"
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />} icon={isCollapsed && !hasNav ? <PanelLeft /> : <LogoSvg />}
style={{ maxHeight: 40 }} style={{ maxHeight: 40 }}
> >
{!isCollapsed && !hasNav && <PanelButton />} {!isCollapsed && !hasNav && <PanelButton />}

View file

@ -13,7 +13,7 @@ import {
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useConfig, useLinkQuery } from '@/components/hooks'; import { useConfig, useLinkQuery } from '@/components/hooks';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Refresh } from '@/components/icons'; import { RefreshCw } from '@/components/icons';
import { getRandomChars } from '@/lib/generate'; import { getRandomChars } from '@/lib/generate';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { LINKS_URL } from '@/lib/constants'; import { LINKS_URL } from '@/lib/constants';
@ -127,7 +127,7 @@ export function LinkEditForm({
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })} onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
> >
<Icon> <Icon>
<Refresh /> <RefreshCw />
</Icon> </Icon>
</Button> </Button>
</Row> </Row>

View file

@ -12,7 +12,7 @@ import {
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useConfig, usePixelQuery } from '@/components/hooks'; import { useConfig, usePixelQuery } from '@/components/hooks';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Refresh } from '@/components/icons'; import { RefreshCw } from '@/components/icons';
import { getRandomChars } from '@/lib/generate'; import { getRandomChars } from '@/lib/generate';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -111,7 +111,7 @@ export function PixelEditForm({
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })} onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
> >
<Icon> <Icon>
<Refresh /> <RefreshCw />
</Icon> </Icon>
</Button> </Button>
</Row> </Row>

View file

@ -1,7 +1,7 @@
import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog, Box } from '@umami/react-zen'; import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog, Box } from '@umami/react-zen';
import { useMessages, useResultQuery } from '@/components/hooks'; import { useMessages, useResultQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { File, Lightning, User } from '@/components/icons'; import { File, LightningSvg, User } from '@/components/icons';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { ReportEditButton } from '@/components/input/ReportEditButton'; import { ReportEditButton } from '@/components/input/ReportEditButton';
import { FunnelEditForm } from './FunnelEditForm'; import { FunnelEditForm } from './FunnelEditForm';
@ -92,7 +92,7 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
</Row> </Row>
<Row alignItems="center" justifyContent="space-between" gap> <Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon>{type === 'path' ? <File /> : <Lightning />}</Icon> <Icon>{type === 'path' ? <File /> : <LightningSvg />}</Icon>
<Text>{value}</Text> <Text>{value}</Text>
</Row> </Row>
<Row alignItems="center" gap> <Row alignItems="center" gap>

View file

@ -14,7 +14,7 @@ import {
Column, Column,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks'; import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks';
import { Close, Plus } from '@/components/icons'; import { X, Plus } from '@/components/icons';
import { ActionSelect } from '@/components/input/ActionSelect'; import { ActionSelect } from '@/components/input/ActionSelect';
import { LookupField } from '@/components/input/LookupField'; import { LookupField } from '@/components/input/LookupField';
@ -36,6 +36,8 @@ export function FunnelEditForm({
const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
const handleSubmit = async ({ name, ...parameters }) => { const handleSubmit = async ({ name, ...parameters }) => {
//
mutate( mutate(
{ ...data, id, name, type: 'funnel', websiteId, parameters }, { ...data, id, name, type: 'funnel', websiteId, parameters },
{ {
@ -75,7 +77,13 @@ export function FunnelEditForm({
> >
<TextField /> <TextField />
</FormField> </FormField>
<FormFieldArray name="steps" label={formatMessage(labels.steps)}> <FormFieldArray
name="steps"
label={formatMessage(labels.steps)}
rules={{
validate: value => value.length > 1 || 'At least two steps are required',
}}
>
{({ fields, append, remove, getValues }) => { {({ fields, append, remove, getValues }) => {
return ( return (
<Grid gap> <Grid gap>
@ -104,7 +112,7 @@ export function FunnelEditForm({
</Column> </Column>
<Button onPress={() => remove(index)}> <Button onPress={() => remove(index)}>
<Icon size="sm"> <Icon size="sm">
<Close /> <X />
</Icon> </Icon>
</Button> </Button>
</Grid> </Grid>

View file

@ -1,7 +1,7 @@
import { Grid, Row, Column, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen'; import { Grid, Row, Column, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen';
import { ReportEditButton } from '@/components/input/ReportEditButton'; import { ReportEditButton } from '@/components/input/ReportEditButton';
import { useMessages, useResultQuery } from '@/components/hooks'; import { useMessages, useResultQuery } from '@/components/hooks';
import { File, Lightning, User } from '@/components/icons'; import { File, LightningSvg, User } from '@/components/icons';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { GoalEditForm } from './GoalEditForm'; import { GoalEditForm } from './GoalEditForm';
@ -68,7 +68,7 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
</Row> </Row>
<Row alignItems="center" justifyContent="space-between" gap> <Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon>{parameters.type === 'path' ? <File /> : <Lightning />}</Icon> <Icon>{parameters.type === 'path' ? <File /> : <LightningSvg />}</Icon>
<Text>{parameters.value}</Text> <Text>{parameters.value}</Text>
</Row> </Row>
<Row alignItems="center" gap> <Row alignItems="center" gap>

View file

@ -3,7 +3,7 @@ import { TooltipTrigger, Tooltip, Focusable, Icon, Text, Row, Column } from '@um
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks'; import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks';
import { File, Lightning } from '@/components/icons'; import { File, LightningSvg } from '@/components/icons';
import { objectToArray } from '@/lib/data'; import { objectToArray } from '@/lib/data';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
@ -215,7 +215,7 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
onClick={() => handleClick(name, columnIndex, paths)} onClick={() => handleClick(name, columnIndex, paths)}
> >
<Row alignItems="center" className={styles.name} title={name} gap> <Row alignItems="center" className={styles.name} title={name} gap>
<Icon>{name.startsWith('/') ? <File /> : <Lightning />}</Icon> <Icon>{name.startsWith('/') ? <File /> : <LightningSvg />}</Icon>
<Text truncate>{name}</Text> <Text truncate>{name}</Text>
</Row> </Row>
<div className={styles.count} title={nodeCount}> <div className={styles.count} title={nodeCount}>

View file

@ -2,6 +2,27 @@ import { Grid, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable'; import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
import {
LogOut,
LogIn,
Search,
Type,
SquareSlash,
SquareArrowRight,
Megaphone,
Earth,
Globe,
Landmark,
MapPin,
AppWindow,
Laptop,
Languages,
Monitor,
Cpu,
LightningSvg,
LucideCaseSensitive,
Tag,
} from '@/components/icons';
export function WebsiteExpandedView({ export function WebsiteExpandedView({
websiteId, websiteId,
@ -24,26 +45,31 @@ export function WebsiteExpandedView({
id: 'path', id: 'path',
label: formatMessage(labels.path), label: formatMessage(labels.path),
path: updateParams({ view: 'path' }), path: updateParams({ view: 'path' }),
icon: <SquareSlash />,
}, },
{ {
id: 'entry', id: 'entry',
label: formatMessage(labels.entry), label: formatMessage(labels.entry),
path: updateParams({ view: 'entry' }), path: updateParams({ view: 'entry' }),
icon: <LogIn />,
}, },
{ {
id: 'exit', id: 'exit',
label: formatMessage(labels.exit), label: formatMessage(labels.exit),
path: updateParams({ view: 'exit' }), path: updateParams({ view: 'exit' }),
icon: <LogOut />,
}, },
{ {
id: 'title', id: 'title',
label: formatMessage(labels.title), label: formatMessage(labels.title),
path: updateParams({ view: 'title' }), path: updateParams({ view: 'title' }),
icon: <Type />,
}, },
{ {
id: 'query', id: 'query',
label: formatMessage(labels.query), label: formatMessage(labels.query),
path: updateParams({ view: 'query' }), path: updateParams({ view: 'query' }),
icon: <Search />,
}, },
], ],
}, },
@ -54,16 +80,19 @@ export function WebsiteExpandedView({
id: 'referrer', id: 'referrer',
label: formatMessage(labels.referrer), label: formatMessage(labels.referrer),
path: updateParams({ view: 'referrer' }), path: updateParams({ view: 'referrer' }),
icon: <SquareArrowRight />,
}, },
{ {
id: 'channel', id: 'channel',
label: formatMessage(labels.channel), label: formatMessage(labels.channel),
path: updateParams({ view: 'channel' }), path: updateParams({ view: 'channel' }),
icon: <Megaphone />,
}, },
{ {
id: 'domain', id: 'domain',
label: formatMessage(labels.domain), label: formatMessage(labels.domain),
path: updateParams({ view: 'domain' }), path: updateParams({ view: 'domain' }),
icon: <Globe />,
}, },
], ],
}, },
@ -74,16 +103,19 @@ export function WebsiteExpandedView({
id: 'country', id: 'country',
label: formatMessage(labels.country), label: formatMessage(labels.country),
path: updateParams({ view: 'country' }), path: updateParams({ view: 'country' }),
icon: <Earth />,
}, },
{ {
id: 'region', id: 'region',
label: formatMessage(labels.region), label: formatMessage(labels.region),
path: updateParams({ view: 'region' }), path: updateParams({ view: 'region' }),
icon: <MapPin />,
}, },
{ {
id: 'city', id: 'city',
label: formatMessage(labels.city), label: formatMessage(labels.city),
path: updateParams({ view: 'city' }), path: updateParams({ view: 'city' }),
icon: <Landmark />,
}, },
], ],
}, },
@ -94,26 +126,31 @@ export function WebsiteExpandedView({
id: 'browser', id: 'browser',
label: formatMessage(labels.browser), label: formatMessage(labels.browser),
path: updateParams({ view: 'browser' }), path: updateParams({ view: 'browser' }),
icon: <AppWindow />,
}, },
{ {
id: 'os', id: 'os',
label: formatMessage(labels.os), label: formatMessage(labels.os),
path: updateParams({ view: 'os' }), path: updateParams({ view: 'os' }),
icon: <Cpu />,
}, },
{ {
id: 'device', id: 'device',
label: formatMessage(labels.device), label: formatMessage(labels.device),
path: updateParams({ view: 'device' }), path: updateParams({ view: 'device' }),
icon: <Laptop />,
}, },
{ {
id: 'language', id: 'language',
label: formatMessage(labels.language), label: formatMessage(labels.language),
path: updateParams({ view: 'language' }), path: updateParams({ view: 'language' }),
icon: <Languages />,
}, },
{ {
id: 'screen', id: 'screen',
label: formatMessage(labels.screen), label: formatMessage(labels.screen),
path: updateParams({ view: 'screen' }), path: updateParams({ view: 'screen' }),
icon: <Monitor />,
}, },
], ],
}, },
@ -124,16 +161,19 @@ export function WebsiteExpandedView({
id: 'event', id: 'event',
label: formatMessage(labels.event), label: formatMessage(labels.event),
path: updateParams({ view: 'event' }), path: updateParams({ view: 'event' }),
icon: <LightningSvg />,
}, },
{ {
id: 'hostname', id: 'hostname',
label: formatMessage(labels.hostname), label: formatMessage(labels.hostname),
path: updateParams({ view: 'hostname' }), path: updateParams({ view: 'hostname' }),
icon: <LucideCaseSensitive />,
}, },
{ {
id: 'tag', id: 'tag',
label: formatMessage(labels.tag), label: formatMessage(labels.tag),
path: updateParams({ view: 'tag' }), path: updateParams({ view: 'tag' }),
icon: <Tag />,
}, },
], ],
}, },
@ -142,7 +182,7 @@ export function WebsiteExpandedView({
return ( return (
<Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden"> <Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden">
<Column gap="6" border="right" paddingRight="3"> <Column gap="6" border="right" paddingRight="3">
<SideMenu items={items} selectedKey={view} /> <SideMenu items={items} selectedKey={view} muteItems={false} />
</Column> </Column>
<Column overflow="hidden"> <Column overflow="hidden">
<MetricsExpandedTable <MetricsExpandedTable

View file

@ -1,19 +1,19 @@
import { import {
Eye, Eye,
Lightning, LightningSvg,
User, User,
Clock, Clock,
Sheet, Sheet,
Target, TargetSvg,
Funnel, FunnelSvg,
Path, PathSvg,
Magnet, MagnetSvg,
Tag, Tag,
Money, MoneySvg,
Network, Network,
ChartPie, ChartPie,
UserPlus, UserPlus,
Compare, CompareSvg,
} from '@/components/icons'; } from '@/components/icons';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
@ -44,7 +44,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'events', id: 'events',
label: formatMessage(labels.events), label: formatMessage(labels.events),
icon: <Lightning />, icon: <LightningSvg />,
path: renderPath('/events'), path: renderPath('/events'),
}, },
{ {
@ -62,7 +62,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'compare', id: 'compare',
label: formatMessage(labels.compare), label: formatMessage(labels.compare),
icon: <Compare />, icon: <CompareSvg />,
path: renderPath('/compare'), path: renderPath('/compare'),
}, },
{ {
@ -79,25 +79,25 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'goals', id: 'goals',
label: formatMessage(labels.goals), label: formatMessage(labels.goals),
icon: <Target />, icon: <TargetSvg />,
path: renderPath('/goals'), path: renderPath('/goals'),
}, },
{ {
id: 'funnel', id: 'funnel',
label: formatMessage(labels.funnels), label: formatMessage(labels.funnels),
icon: <Funnel />, icon: <FunnelSvg />,
path: renderPath('/funnels'), path: renderPath('/funnels'),
}, },
{ {
id: 'journeys', id: 'journeys',
label: formatMessage(labels.journeys), label: formatMessage(labels.journeys),
icon: <Path />, icon: <PathSvg />,
path: renderPath('/journeys'), path: renderPath('/journeys'),
}, },
{ {
id: 'retention', id: 'retention',
label: formatMessage(labels.retention), label: formatMessage(labels.retention),
icon: <Magnet />, icon: <MagnetSvg />,
path: renderPath('/retention'), path: renderPath('/retention'),
}, },
], ],
@ -131,7 +131,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'revenue', id: 'revenue',
label: formatMessage(labels.revenue), label: formatMessage(labels.revenue),
icon: <Money />, icon: <MoneySvg />,
path: renderPath('/revenue'), path: renderPath('/revenue'),
}, },
{ {

View file

@ -2,7 +2,7 @@ import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen'; import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks'; import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon'; import { TypeIcon } from '@/components/common/TypeIcon';
import { Location, KeyRound, Calendar } from '@/components/icons'; import { LocationSvg, KeyRound, Calendar } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance'; import { DateDistance } from '@/components/common/DateDistance';
export function SessionInfo({ data }) { export function SessionInfo({ data }) {
@ -36,11 +36,11 @@ export function SessionInfo({ data }) {
{formatValue(data?.country, 'country')} {formatValue(data?.country, 'country')}
</Info> </Info>
<Info label={formatMessage(labels.region)} icon={<Location />}> <Info label={formatMessage(labels.region)} icon={<LocationSvg />}>
{getRegionName(data?.region)} {getRegionName(data?.region)}
</Info> </Info>
<Info label={formatMessage(labels.city)} icon={<Location />}> <Info label={formatMessage(labels.city)} icon={<LocationSvg />}>
{data?.city} {data?.city}
</Info> </Info>

View file

@ -2,8 +2,9 @@ import { z } from 'zod';
import * as send from '@/app/api/send/route'; import * as send from '@/app/api/send/route';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json, serverError } from '@/lib/response'; import { json, serverError } from '@/lib/response';
import { anyObjectParam } from '@/lib/schema';
const schema = z.array(z.object({}).passthrough()); const schema = z.array(anyObjectParam);
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {

View file

@ -1,7 +1,7 @@
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions'; import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json, notFound, ok, unauthorized } from '@/lib/response'; import { json, notFound, ok, unauthorized } from '@/lib/response';
import { segmentTypeParam } from '@/lib/schema'; import { anyObjectParam, segmentTypeParam } from '@/lib/schema';
import { deleteSegment, getSegment, updateSegment } from '@/queries'; import { deleteSegment, getSegment, updateSegment } from '@/queries';
import { z } from 'zod'; import { z } from 'zod';
@ -33,7 +33,7 @@ export async function POST(
const schema = z.object({ const schema = z.object({
type: segmentTypeParam, type: segmentTypeParam,
name: z.string().max(200), name: z.string().max(200),
parameters: z.object({}).passthrough(), parameters: anyObjectParam,
}); });
const { auth, body, error } = await parseRequest(request, schema); const { auth, body, error } = await parseRequest(request, schema);

View file

@ -2,7 +2,7 @@ import { canUpdateWebsite, canViewWebsite } from '@/permissions';
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { segmentTypeParam, searchParams } from '@/lib/schema'; import { segmentTypeParam, searchParams, anyObjectParam } from '@/lib/schema';
import { createSegment, getWebsiteSegments } from '@/queries'; import { createSegment, getWebsiteSegments } from '@/queries';
import { z } from 'zod'; import { z } from 'zod';
@ -42,7 +42,7 @@ export async function POST(
const schema = z.object({ const schema = z.object({
type: segmentTypeParam, type: segmentTypeParam,
name: z.string().max(200), name: z.string().max(200),
parameters: z.object({}).passthrough(), parameters: anyObjectParam,
}); });
const { auth, body, error } = await parseRequest(request, schema); const { auth, body, error } = await parseRequest(request, schema);

View file

@ -13,7 +13,7 @@ import { useRouter } from 'next/navigation';
import { useMessages, useUpdateQuery } from '@/components/hooks'; import { useMessages, useUpdateQuery } from '@/components/hooks';
import { setUser } from '@/store/app'; import { setUser } from '@/store/app';
import { setClientAuthToken } from '@/lib/client'; import { setClientAuthToken } from '@/lib/client';
import { Logo } from '@/components/icons'; import { LogoSvg } from '@/components/icons';
export function LoginForm() { export function LoginForm() {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
@ -34,7 +34,7 @@ export function LoginForm() {
return ( return (
<Column justifyContent="center" alignItems="center" gap="6"> <Column justifyContent="center" alignItems="center" gap="6">
<Icon size="lg"> <Icon size="lg">
<Logo /> <LogoSvg />
</Icon> </Icon>
<Heading>umami</Heading> <Heading>umami</Heading>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}> <Form onSubmit={handleSubmit} error={getErrorMessage(error)}>

View file

@ -1,6 +1,6 @@
import { Icon, Text, Row } from '@umami/react-zen'; import { Icon, Text, Row } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Alert } from '@/components/icons'; import { AlertTriangle } from '@/components/icons';
export function ErrorMessage() { export function ErrorMessage() {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
@ -8,7 +8,7 @@ export function ErrorMessage() {
return ( return (
<Row alignItems="center" justifyContent="center" gap> <Row alignItems="center" justifyContent="center" gap>
<Icon> <Icon>
<Alert /> <AlertTriangle />
</Icon> </Icon>
<Text>{formatMessage(messages.error)}</Text> <Text>{formatMessage(messages.error)}</Text>
</Row> </Row>

View file

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Grid, Column, TextField, Label, Select, Icon, Button, ListItem } from '@umami/react-zen'; import { Grid, Column, TextField, Label, Select, Icon, Button, ListItem } from '@umami/react-zen';
import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks'; import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
import { Close } from '@/components/icons'; import { X } from '@/components/icons';
import { isSearchOperator } from '@/lib/params'; import { isSearchOperator } from '@/lib/params';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
@ -107,7 +107,7 @@ export function FilterRecord({
<Column justifyContent="flex-end"> <Column justifyContent="flex-end">
<Button variant="quiet" onPress={() => onRemove?.(name)}> <Button variant="quiet" onPress={() => onRemove?.(name)}>
<Icon> <Icon>
<Close /> <X />
</Icon> </Icon>
</Button> </Button>
</Column> </Column>

View file

@ -1,6 +1,6 @@
import { Button, Icon, Row, Text } from '@umami/react-zen'; import { Button, Icon, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Chevron } from '@/components/icons'; import { ChevronRight } from '@/components/icons';
export interface PagerProps { export interface PagerProps {
page: string | number; page: string | number;
@ -45,12 +45,12 @@ export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
<Row gap="1"> <Row gap="1">
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}> <Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}> <Icon size="sm" rotate={180}>
<Chevron /> <ChevronRight />
</Icon> </Icon>
</Button> </Button>
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}> <Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm"> <Icon size="sm">
<Chevron /> <ChevronRight />
</Icon> </Icon>
</Button> </Button>
</Row> </Row>

View file

@ -9,7 +9,7 @@ import {
Tooltip, Tooltip,
Heading, Heading,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { Maximize, Close } from '@/components/icons'; import { Maximize, X } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export interface PanelProps extends ColumnProps { export interface PanelProps extends ColumnProps {
@ -59,7 +59,7 @@ export function Panel({
<Row justifyContent="flex-end" alignItems="center"> <Row justifyContent="flex-end" alignItems="center">
<TooltipTrigger delay={0} isDisabled={isFullscreen}> <TooltipTrigger delay={0} isDisabled={isFullscreen}>
<Button variant="quiet" onPress={handleFullscreen}> <Button variant="quiet" onPress={handleFullscreen}>
<Icon>{isFullscreen ? <Close /> : <Maximize />}</Icon> <Icon>{isFullscreen ? <X /> : <Maximize />}</Icon>
</Button> </Button>
<Tooltip>{formatMessage(labels.maximize)}</Tooltip> <Tooltip>{formatMessage(labels.maximize)}</Tooltip>
</TooltipTrigger> </TooltipTrigger>

View file

@ -1,71 +1,17 @@
export * from 'lucide-react';
export { export {
AlertTriangle as Alert, Logo as LogoSvg,
ArrowRight as Arrow, Bolt as BoltSvg,
Bookmark, Change as ChangeSvg,
Calendar, Compare as CompareSvg,
ChartPie, Funnel as FunnelSvg,
ChevronRight as Chevron, Lightbulb as LightbulbSvg,
Clock, Lightning as LightningSvg,
Copy, Location as LocationSvg,
Database, Magnet as MagnetSvg,
Download, Money as MoneySvg,
Edit, Network as NetworkSvg,
Ellipsis, Path as PathSvg,
Equal, Tag as TagSvg,
Eye, Target as TargetSvg,
ExternalLink,
File,
FileJson,
FileText,
Globe,
Grid2X2 as Pixel,
KeyRound,
LayoutDashboard,
Link,
ListCheck,
ListFilter,
LockKeyhole,
LogOut,
Maximize,
Menu,
Minimize,
Moon,
MoreHorizontal as More,
Paperclip,
PanelLeft,
Plus,
RefreshCw as Refresh,
Settings,
Settings2 as Knobs,
Share,
Sheet,
Slash,
SquarePen,
SquarePlus,
Sun,
Trash,
Upload,
User,
CircleUserRound as UserCircle,
Users,
UserPlus,
X as Close,
} from 'lucide-react';
export {
Logo,
Bolt,
Change,
Compare,
Funnel,
Lightbulb,
Lightning,
Location,
Magnet,
Money,
Network,
Path,
Tag,
Target,
AddUser,
Visitor,
} from '@/components/svg'; } from '@/components/svg';

View file

@ -16,7 +16,7 @@ import {
useFilters, useFilters,
useWebsiteSegmentQuery, useWebsiteSegmentQuery,
} from '@/components/hooks'; } from '@/components/hooks';
import { Close, Bookmark } from '@/components/icons'; import { X, Bookmark } from '@/components/icons';
import { isSearchOperator } from '@/lib/params'; import { isSearchOperator } from '@/lib/params';
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
@ -111,7 +111,7 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
<Button variant="zero" onPress={handleResetFilter}> <Button variant="zero" onPress={handleResetFilter}>
<Icon> <Icon>
<Close /> <X />
</Icon> </Icon>
</Button> </Button>
<Tooltip> <Tooltip>
@ -146,7 +146,7 @@ const FilterItem = ({ name, label, operator, value, onRemove }) => {
</Text> </Text>
</Row> </Row>
<Icon onClick={() => onRemove(name)} size="xs" style={{ cursor: 'pointer' }}> <Icon onClick={() => onRemove(name)} size="xs" style={{ cursor: 'pointer' }}>
<Close /> <X />
</Icon> </Icon>
</Row> </Row>
</Row> </Row>

View file

@ -13,7 +13,7 @@ import {
Text, Text,
Row, Row,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { Edit, More, Trash } from '@/components/icons'; import { Edit, MoreHorizontal, Trash } from '@/components/icons';
export function ReportEditButton({ export function ReportEditButton({
id, id,
@ -61,7 +61,7 @@ export function ReportEditButton({
<MenuTrigger> <MenuTrigger>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<More /> <MoreHorizontal />
</Icon> </Icon>
</Button> </Button>
<Popover placement="bottom"> <Popover placement="bottom">
@ -90,9 +90,7 @@ export function ReportEditButton({
onCancel={handleClose} onCancel={handleClose}
isDanger isDanger
> >
<Row gap="1"> <Row gap="1">{formatMessage(messages.confirmDelete, { target: name })}</Row>
{formatMessage(messages.confirmDelete, { target: <b key={name}>{name}</b> })}
</Row>
</AlertDialog> </AlertDialog>
)} )}
</Modal> </Modal>

View file

@ -13,7 +13,7 @@ import {
Loading, Loading,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks'; import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
import { Chevron, User, Users } from '@/components/icons'; import { ChevronRight, User, Users } from '@/components/icons';
export interface TeamsButtonProps { export interface TeamsButtonProps {
showText?: boolean; showText?: boolean;
@ -50,7 +50,7 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
</Row> </Row>
{showText && ( {showText && (
<Icon rotate={90} size="sm"> <Icon rotate={90} size="sm">
<Chevron /> <ChevronRight />
</Icon> </Icon>
)} )}
</Row> </Row>

View file

@ -1,6 +1,6 @@
import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen'; import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import { Chevron } from '@/components/icons'; import { ChevronRight } from '@/components/icons';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { DateFilter } from './DateFilter'; import { DateFilter } from './DateFilter';
@ -49,17 +49,17 @@ export function WebsiteDateFilter({
<Row gap="1"> <Row gap="1">
<Button onPress={() => handleIncrement(-1)} variant="outline"> <Button onPress={() => handleIncrement(-1)} variant="outline">
<Icon rotate={180}> <Icon rotate={180}>
<Chevron /> <ChevronRight />
</Icon> </Icon>
</Button> </Button>
<Button onPress={() => handleIncrement(1)} variant="outline" isDisabled={disableForward}> <Button onPress={() => handleIncrement(1)} variant="outline" isDisabled={disableForward}>
<Icon> <Icon>
<Chevron /> <ChevronRight />
</Icon> </Icon>
</Button> </Button>
</Row> </Row>
)} )}
<Row width="200px"> <Row minWidth="200px">
<DateFilter <DateFilter
value={value} value={value}
onChange={handleChange} onChange={handleChange}

View file

@ -1,6 +1,6 @@
import { Icon, Text, Row, RowProps } from '@umami/react-zen'; import { Icon, Text, Row, RowProps } from '@umami/react-zen';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Arrow } from '@/components/icons'; import { ArrowRight } from '@/components/icons';
const STYLES = { const STYLES = {
positive: { positive: {
@ -51,7 +51,7 @@ export function ChangeLabel({
> >
{!neutral && ( {!neutral && (
<Icon rotate={positive ? -90 : 90} size={size}> <Icon rotate={positive ? -90 : 90} size={size}>
<Arrow /> <ArrowRight />
</Icon> </Icon>
)} )}
<Text>{children || value}</Text> <Text>{children || value}</Text>

View file

@ -2,7 +2,7 @@ import { ReactNode, useState } from 'react';
import { Button, Column, DataColumn, DataTable, Icon, Row, SearchField } from '@umami/react-zen'; import { Button, Column, DataColumn, DataTable, Icon, Row, SearchField } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages, useWebsiteExpandedMetricsQuery } from '@/components/hooks'; import { useMessages, useWebsiteExpandedMetricsQuery } from '@/components/hooks';
import { Close } from '@/components/icons'; import { X } from '@/components/icons';
import { DownloadButton } from '@/components/input/DownloadButton'; import { DownloadButton } from '@/components/input/DownloadButton';
import { formatShortTime } from '@/lib/format'; import { formatShortTime } from '@/lib/format';
import { MetricLabel } from '@/components/metrics/MetricLabel'; import { MetricLabel } from '@/components/metrics/MetricLabel';
@ -55,7 +55,7 @@ export function MetricsExpandedTable({
{onClose && ( {onClose && (
<Button onPress={onClose} variant="quiet"> <Button onPress={onClose} variant="quiet">
<Icon> <Icon>
<Close /> <X />
</Icon> </Icon>
</Button> </Button>
)} )}

View file

@ -57,7 +57,7 @@ export const userRoleParam = z.enum(['admin', 'user', 'view-only']);
export const teamRoleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); export const teamRoleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
export const anyObjectParam = z.object({}).passthrough(); export const anyObjectParam = z.record(z.string(), z.any());
export const urlOrPathParam = z.string().refine( export const urlOrPathParam = z.string().refine(
value => { value => {
@ -202,7 +202,7 @@ export const reportBaseSchema = z.object({
type: reportTypeParam, type: reportTypeParam,
name: z.string().max(200), name: z.string().max(200),
description: z.string().max(500).optional(), description: z.string().max(500).optional(),
parameters: z.object({}).passthrough(), parameters: anyObjectParam,
}); });
export const reportTypeSchema = z.discriminatedUnion('type', [ export const reportTypeSchema = z.discriminatedUnion('type', [

View file

@ -14,6 +14,10 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
const website = await getWebsite(websiteId); const website = await getWebsite(websiteId);
if (!website) {
return false;
}
if (website.userId) { if (website.userId) {
return user.id === website.userId; return user.id === website.userId;
} }
@ -46,6 +50,10 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
const website = await getWebsite(websiteId); const website = await getWebsite(websiteId);
if (!website) {
return false;
}
if (website.userId) { if (website.userId) {
return user.id === website.userId; return user.id === website.userId;
} }
@ -66,6 +74,10 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
const website = await getWebsite(websiteId); const website = await getWebsite(websiteId);
if (!website) {
return false;
}
if (website.userId) { if (website.userId) {
return user.id === website.userId; return user.id === website.userId;
} }
@ -82,6 +94,10 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) { export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
const website = await getWebsite(websiteId); const website = await getWebsite(websiteId);
if (!website) {
return false;
}
if (website.teamId && user.id === userId) { if (website.teamId && user.id === userId) {
const teamUser = await getTeamUser(website.teamId, userId); const teamUser = await getTeamUser(website.teamId, userId);
@ -94,6 +110,10 @@ export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) { export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
const website = await getWebsite(websiteId); const website = await getWebsite(websiteId);
if (!website) {
return false;
}
if (website.userId && website.userId === user.id) { if (website.userId && website.userId === user.id) {
const teamUser = await getTeamUser(teamId, user.id); const teamUser = await getTeamUser(teamId, user.id);

View file

@ -11,17 +11,9 @@ import {
import { getItem } from '@/lib/storage'; import { getItem } from '@/lib/storage';
import { getTimezone } from '@/lib/date'; import { getTimezone } from '@/lib/date';
function getDefaultTheme() {
return typeof window !== 'undefined'
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
? 'dark'
: 'light'
: 'light';
}
const initialState = { const initialState = {
locale: getItem(LOCALE_CONFIG) || process.env.defaultLocale || DEFAULT_LOCALE, locale: getItem(LOCALE_CONFIG) || process.env.defaultLocale || DEFAULT_LOCALE,
theme: getItem(THEME_CONFIG) || getDefaultTheme() || DEFAULT_THEME, theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(), timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE, dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
shareToken: null, shareToken: null,