diff --git a/next.config.ts b/next.config.ts index 77a0c1c6..278e7a6d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -97,7 +97,12 @@ const headers = [ }, ]; -const rewrites = []; +const rewrites = [ + { + source: '/teams/:id/settings/:path*', + destination: '/settings/:path*', + }, +]; if (trackerScriptURL) { rewrites.push({ diff --git a/package.json b/package.json index bf0bad76..c2c9b3cc 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.154.0", + "@umami/react-zen": "^0.157.0", "@umami/redis-client": "^0.27.0", "bcryptjs": "^3.0.2", "chalk": "^5.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd379179..fce70ca0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.83.0 version: 5.83.0(react@19.1.0) '@umami/react-zen': - specifier: ^0.154.0 - version: 0.154.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0)) + specifier: ^0.157.0 + version: 0.157.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0)) '@umami/redis-client': specifier: ^0.27.0 version: 0.27.0 @@ -2544,8 +2544,8 @@ packages: resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@umami/react-zen@0.154.0': - resolution: {integrity: sha512-waVMJNwGPSDereFWTnbtdJjQGQhJIi1n7Hs2EnuS9OSMUbj9THgA1A9722zyO3w46KmbIBMqmIxur4ykpHOYlg==} + '@umami/react-zen@0.157.0': + resolution: {integrity: sha512-QZqZcah1t6KniDvFTF+GxBDkaEYUuQOyj1olao5p5rtZBMW1/v4GaVcL+8UAzWyWIUnsrRI3e5HoytapJg6VfQ==} '@umami/redis-client@0.27.0': resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==} @@ -5984,8 +5984,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-hook-form@7.61.1: - resolution: {integrity: sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==} + react-hook-form@7.62.0: + resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -9751,7 +9751,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@umami/react-zen@0.154.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))': + '@umami/react-zen@0.157.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))': dependencies: '@fontsource/jetbrains-mono': 5.2.6 '@internationalized/date': 3.8.2 @@ -9765,7 +9765,7 @@ snapshots: react: 19.1.0 react-aria-components: 1.9.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) - react-hook-form: 7.61.1(react@19.1.0) + react-hook-form: 7.62.0(react@19.1.0) react-icons: 5.5.0(react@19.1.0) thenby: 1.3.4 zustand: 5.0.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) @@ -13763,7 +13763,7 @@ snapshots: dependencies: react: 19.1.0 - react-hook-form@7.61.1(react@19.1.0): + react-hook-form@7.62.0(react@19.1.0): dependencies: react: 19.1.0 diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index e3602d5a..bb33dc87 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -4,7 +4,6 @@ import Script from 'next/script'; import { usePathname } from 'next/navigation'; import { UpdateNotice } from './UpdateNotice'; import { SideNav } from '@/app/(main)/SideNav'; -import { TopNav } from '@/app/(main)/TopNav'; import { useLoginQuery, useConfig } from '@/components/hooks'; export function App({ children }) { @@ -31,7 +30,6 @@ export function App({ children }) { - {children} diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index 25f5a4ab..06b0abc9 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -17,15 +17,20 @@ import { LockKeyhole, } from '@/components/icons'; import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; -import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; import { TeamsButton } from '@/components/input/TeamsButton'; import { PanelButton } from '@/components/input/PanelButton'; export function SideNav(props: SidebarProps) { const { formatMessage, labels } = useMessages(); - const { pathname, renderUrl, websiteId, teamId } = useNavigation(); + const { pathname, renderUrl, websiteId } = useNavigation(); const [isCollapsed] = useGlobalState('sidenav-collapsed'); - const isWebsite = websiteId && !pathname.includes('/settings'); + + const hasNav = !!( + websiteId || + pathname.startsWith('/admin') || + pathname.startsWith('/settings') || + pathname.endsWith('/settings') + ); const links = [ { @@ -71,12 +76,7 @@ export function SideNav(props: SidebarProps) { return ( - + } /> @@ -90,23 +90,21 @@ export function SideNav(props: SidebarProps) { })} - {!teamId && - bottomLinks.map(({ id, path, label, icon }) => { - return ( - - - - ); - })} + {bottomLinks.map(({ id, path, label, icon }) => { + return ( + + + + ); + })} - + - {isWebsite && } ); } diff --git a/src/app/(main)/admin/AdminLayout.tsx b/src/app/(main)/admin/AdminLayout.tsx index 9dfc118c..87abd77b 100644 --- a/src/app/(main)/admin/AdminLayout.tsx +++ b/src/app/(main)/admin/AdminLayout.tsx @@ -2,9 +2,8 @@ import { ReactNode } from 'react'; import { Grid, Column } from '@umami/react-zen'; import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks'; +import { User, Users, Globe } from '@/components/icons'; import { SideMenu } from '@/components/common/SideMenu'; -import { PageHeader } from '@/components/common/PageHeader'; -import { Panel } from '@/components/common/Panel'; import { PageBody } from '@/components/common/PageBody'; export function AdminLayout({ children }: { children: ReactNode }) { @@ -18,37 +17,45 @@ export function AdminLayout({ children }: { children: ReactNode }) { const items = [ { - id: 'users', - label: formatMessage(labels.users), - url: '/admin/users', - }, - { - id: 'websites', - label: formatMessage(labels.websites), - url: '/admin/websites', - }, - { - id: 'teams', - label: formatMessage(labels.teams), - url: '/admin/teams', + label: formatMessage(labels.application), + items: [ + { + id: 'users', + label: formatMessage(labels.users), + path: '/admin/users', + icon: , + }, + { + id: 'websites', + label: formatMessage(labels.websites), + path: '/admin/websites', + icon: , + }, + { + id: 'teams', + label: formatMessage(labels.teams), + path: '/admin/teams', + icon: , + }, + ], }, ]; - const value = items.find(({ url }) => pathname.includes(url))?.id; + const selectedKey = items + .flatMap(e => e.items) + ?.find(({ path }) => path && pathname.startsWith(path))?.id; return ( - - - - - - - - - {children} - - + + + - + {children} + ); } diff --git a/src/app/(main)/admin/teams/AdminTeamsPage.tsx b/src/app/(main)/admin/teams/AdminTeamsPage.tsx index 37f43c94..1d4984e8 100644 --- a/src/app/(main)/admin/teams/AdminTeamsPage.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsPage.tsx @@ -1,16 +1,19 @@ 'use client'; import { AdminTeamsDataTable } from './AdminTeamsDataTable'; import { Column } from '@umami/react-zen'; -import { SectionHeader } from '@/components/common/SectionHeader'; import { useMessages } from '@/components/hooks'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; export function AdminTeamsPage() { const { formatMessage, labels } = useMessages(); return ( - - - + + + + + ); } diff --git a/src/app/(main)/admin/teams/AdminTeamsTable.tsx b/src/app/(main)/admin/teams/AdminTeamsTable.tsx index da72955d..bacb7de8 100644 --- a/src/app/(main)/admin/teams/AdminTeamsTable.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsTable.tsx @@ -32,7 +32,9 @@ export function AdminTeamsTable({ {(row: any) => ( - {row?.teamUser?.[0]?.user?.username} + + {row?.teamUser?.[0]?.user?.username} + )} diff --git a/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx b/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx index 7f680a91..a7c5b0e2 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 { TeamDetails } from '@/app/(main)/settings/teams/[teamId]/TeamDetails'; -import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { TeamProvider } from '@/app/(main)/settings/teams/[teamId]/TeamProvider'; export function AdminTeamPage({ teamId }: { teamId: string }) { return ( diff --git a/src/app/(main)/admin/users/UsersPage.tsx b/src/app/(main)/admin/users/UsersPage.tsx index 5c8a1ae3..47ac5512 100644 --- a/src/app/(main)/admin/users/UsersPage.tsx +++ b/src/app/(main)/admin/users/UsersPage.tsx @@ -1,9 +1,10 @@ 'use client'; import { UsersDataTable } from './UsersDataTable'; import { Column } from '@umami/react-zen'; -import { SectionHeader } from '@/components/common/SectionHeader'; import { useMessages } from '@/components/hooks'; import { UserAddButton } from './UserAddButton'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; export function UsersPage() { const { formatMessage, labels } = useMessages(); @@ -11,11 +12,13 @@ export function UsersPage() { const handleSave = () => {}; return ( - - + + - - + + + + ); } diff --git a/src/app/(main)/admin/users/[userId]/UserHeader.tsx b/src/app/(main)/admin/users/[userId]/UserHeader.tsx new file mode 100644 index 00000000..f9fcdd64 --- /dev/null +++ b/src/app/(main)/admin/users/[userId]/UserHeader.tsx @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { User } from '@/components/icons'; +import { PageHeader } from '@/components/common/PageHeader'; +import { UserContext } from '@/app/(main)/admin/users/[userId]/UserProvider'; + +export function UserHeader() { + const user = useContext(UserContext); + + return } />; +} diff --git a/src/app/(main)/admin/users/[userId]/UserPage.tsx b/src/app/(main)/admin/users/[userId]/UserPage.tsx index 56812c0c..0ae65855 100644 --- a/src/app/(main)/admin/users/[userId]/UserPage.tsx +++ b/src/app/(main)/admin/users/[userId]/UserPage.tsx @@ -1,11 +1,19 @@ 'use client'; +import { Column } from '@umami/react-zen'; import { UserSettings } from './UserSettings'; import { UserProvider } from './UserProvider'; +import { UserHeader } from '@/app/(main)/admin/users/[userId]/UserHeader'; +import { Panel } from '@/components/common/Panel'; export function UserPage({ userId }: { userId: string }) { return ( - + + + + + + ); } diff --git a/src/app/(main)/admin/users/[userId]/UserSettings.tsx b/src/app/(main)/admin/users/[userId]/UserSettings.tsx index 8369c3f4..3d699ebe 100644 --- a/src/app/(main)/admin/users/[userId]/UserSettings.tsx +++ b/src/app/(main)/admin/users/[userId]/UserSettings.tsx @@ -1,19 +1,13 @@ -import { useContext } from 'react'; -import { Tabs, Tab, TabList, TabPanel } from '@umami/react-zen'; -import { User } from '@/components/icons'; +import { Column, Tabs, Tab, TabList, TabPanel } from '@umami/react-zen'; import { UserEditForm } from './UserEditForm'; -import { SectionHeader } from '@/components/common/SectionHeader'; import { useMessages } from '@/components/hooks'; import { UserWebsites } from './UserWebsites'; -import { UserContext } from './UserProvider'; export function UserSettings({ userId }: { userId: string }) { const { formatMessage, labels } = useMessages(); - const user = useContext(UserContext); return ( - <> - } /> + {formatMessage(labels.details)} @@ -26,6 +20,6 @@ export function UserSettings({ userId }: { userId: string }) { - + ); } diff --git a/src/app/(main)/admin/websites/AdminWebsitesPage.tsx b/src/app/(main)/admin/websites/AdminWebsitesPage.tsx index 07a73a40..3ca61f78 100644 --- a/src/app/(main)/admin/websites/AdminWebsitesPage.tsx +++ b/src/app/(main)/admin/websites/AdminWebsitesPage.tsx @@ -1,16 +1,19 @@ 'use client'; import { AdminWebsitesDataTable } from './AdminWebsitesDataTable'; import { Column } from '@umami/react-zen'; -import { SectionHeader } from '@/components/common/SectionHeader'; import { useMessages } from '@/components/hooks'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; export function AdminWebsitesPage() { const { formatMessage, labels } = useMessages(); return ( - - - + + + + + ); } diff --git a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx index 7b970e7f..81a6aacf 100644 --- a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx +++ b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx @@ -55,7 +55,7 @@ export function AdminWebsitesTable({ data = [] }: { data: any[] }) { return ( - + diff --git a/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx b/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx index 637da839..da79026b 100644 --- a/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx +++ b/src/app/(main)/admin/websites/[websiteId]/AdminWebsitePage.tsx @@ -1,11 +1,14 @@ 'use client'; import { WebsiteSettings } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettings'; import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { Panel } from '@/components/common/Panel'; export function AdminWebsitePage({ websiteId }: { websiteId: string }) { return ( - + + + ); } diff --git a/src/app/(main)/admin/websites/[websiteId]/page.tsx b/src/app/(main)/admin/websites/[websiteId]/page.tsx index ed10ec43..e1c92276 100644 --- a/src/app/(main)/admin/websites/[websiteId]/page.tsx +++ b/src/app/(main)/admin/websites/[websiteId]/page.tsx @@ -1,10 +1,10 @@ -import { AdminWebsitePage } from './AdminWebsitePage'; import { Metadata } from 'next'; +import { WebsiteSettingsPage } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage'; 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)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx index e12246e3..9617049f 100644 --- a/src/app/(main)/settings/SettingsLayout.tsx +++ b/src/app/(main)/settings/SettingsLayout.tsx @@ -2,44 +2,60 @@ import { ReactNode } from 'react'; import { Grid, Column } from '@umami/react-zen'; import { useMessages, useNavigation } from '@/components/hooks'; -import { SideMenu } from '@/components/common/SideMenu'; -import { PageHeader } from '@/components/common/PageHeader'; -import { Panel } from '@/components/common/Panel'; import { PageBody } from '@/components/common/PageBody'; +import { SideMenu } from '@/components/common/SideMenu'; +import { UserCircle, Users, Knobs } from '@/components/icons'; export function SettingsLayout({ children }: { children: ReactNode }) { const { formatMessage, labels } = useMessages(); - const { pathname } = useNavigation(); + const { renderUrl, pathname } = useNavigation(); const items = [ { - id: 'preferences', - label: formatMessage(labels.preferences), - url: '/settings/preferences', + label: formatMessage(labels.application), + items: [ + { + id: 'preferences', + label: formatMessage(labels.preferences), + path: renderUrl('/settings/preferences'), + icon: , + }, + { + id: 'teams', + label: formatMessage(labels.teams), + path: renderUrl('/settings/teams'), + icon: , + }, + ], }, { - id: 'profile', - label: formatMessage(labels.profile), - url: '/settings/profile', + label: formatMessage(labels.account), + items: [ + { + id: 'profile', + label: formatMessage(labels.profile), + path: renderUrl('/settings/profile'), + icon: , + }, + ], }, - { id: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' }, ]; - const value = items.find(({ url }) => pathname.includes(url))?.id; + const selectedKey = + items.flatMap(e => e.items)?.find(({ path }) => path && pathname.endsWith(path))?.id || + 'overview'; return ( - - - - - - - - - {children} - - + + + - + {children} + ); } diff --git a/src/app/(main)/settings/preferences/PreferenceSettings.tsx b/src/app/(main)/settings/preferences/PreferenceSettings.tsx index c956ebad..9192a905 100644 --- a/src/app/(main)/settings/preferences/PreferenceSettings.tsx +++ b/src/app/(main)/settings/preferences/PreferenceSettings.tsx @@ -19,17 +19,14 @@ export function PreferenceSettings() { - - - - - - - + + + + diff --git a/src/app/(main)/settings/preferences/PreferencesPage.tsx b/src/app/(main)/settings/preferences/PreferencesPage.tsx index 62d7f882..99289956 100644 --- a/src/app/(main)/settings/preferences/PreferencesPage.tsx +++ b/src/app/(main)/settings/preferences/PreferencesPage.tsx @@ -1,16 +1,19 @@ 'use client'; import { Column } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; -import { SectionHeader } from '@/components/common/SectionHeader'; +import { Panel } from '@/components/common/Panel'; import { PreferenceSettings } from './PreferenceSettings'; +import { PageHeader } from '@/components/common/PageHeader'; export function PreferencesPage() { const { formatMessage, labels } = useMessages(); return ( - - - + + + + + ); } diff --git a/src/app/(main)/settings/profile/ProfilePage.tsx b/src/app/(main)/settings/profile/ProfilePage.tsx index aaec3f58..2d1ce0d3 100644 --- a/src/app/(main)/settings/profile/ProfilePage.tsx +++ b/src/app/(main)/settings/profile/ProfilePage.tsx @@ -1,16 +1,19 @@ 'use client'; import { ProfileSettings } from './ProfileSettings'; import { useMessages } from '@/components/hooks'; -import { SectionHeader } from '@/components/common/SectionHeader'; +import { Panel } from '@/components/common/Panel'; import { Column } from '@umami/react-zen'; +import { PageHeader } from '@/components/common/PageHeader'; export function ProfilePage() { const { formatMessage, labels } = useMessages(); return ( - - - + + + + + ); } diff --git a/src/app/(main)/settings/profile/ProfileSettings.tsx b/src/app/(main)/settings/profile/ProfileSettings.tsx index 72ae7d91..b580d686 100644 --- a/src/app/(main)/settings/profile/ProfileSettings.tsx +++ b/src/app/(main)/settings/profile/ProfileSettings.tsx @@ -34,12 +34,10 @@ export function ProfileSettings() { {username} - {renderRole(role)} - {!cloudMode && ( diff --git a/src/app/(main)/settings/teams/TeamsHeader.tsx b/src/app/(main)/settings/teams/TeamsHeader.tsx index 96774f5e..61244033 100644 --- a/src/app/(main)/settings/teams/TeamsHeader.tsx +++ b/src/app/(main)/settings/teams/TeamsHeader.tsx @@ -1,5 +1,5 @@ import { Row } from '@umami/react-zen'; -import { SectionHeader } from '@/components/common/SectionHeader'; +import { PageHeader } from '@/components/common/PageHeader'; import { ROLES } from '@/lib/constants'; import { useLoginQuery, useMessages } from '@/components/hooks'; import { TeamsJoinButton } from './TeamsJoinButton'; @@ -11,11 +11,11 @@ export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) { const cloudMode = !!process.env.cloudMode; return ( - + {!cloudMode && } {allowCreate && user.role !== ROLES.viewOnly && } - + ); } diff --git a/src/app/(main)/settings/teams/TeamsSettingsPage.tsx b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx index e64461c0..57e4aad8 100644 --- a/src/app/(main)/settings/teams/TeamsSettingsPage.tsx +++ b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx @@ -2,12 +2,15 @@ import { TeamsDataTable } from './TeamsDataTable'; import { TeamsHeader } from './TeamsHeader'; import { Column } from '@umami/react-zen'; +import { Panel } from '@/components/common/Panel'; export function TeamsSettingsPage() { return ( - + - + + + ); } diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/settings/teams/TeamsTable.tsx index bfe6b332..a2599251 100644 --- a/src/app/(main)/settings/teams/TeamsTable.tsx +++ b/src/app/(main)/settings/teams/TeamsTable.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; export function TeamsTable({ data = [], - showActions = true, + showActions = false, }: { data: any[]; allowEdit?: boolean; @@ -29,7 +29,7 @@ export function TeamsTable({ {(row: any) => row._count.teamUser} - {showActions && ( + {showActions ? ( {(row: any) => { const { id } = row; @@ -56,7 +56,7 @@ export function TeamsTable({ ); }} - )} + ) : null} ); } diff --git a/src/app/(main)/settings/teams/[teamId]/TeamDetails.tsx b/src/app/(main)/settings/teams/[teamId]/TeamDetails.tsx index 071c56a8..e4900004 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamDetails.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamDetails.tsx @@ -1,8 +1,8 @@ import { useContext, useState } from 'react'; import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen'; -import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { TeamContext } from '@/app/(main)/settings/teams/[teamId]/TeamProvider'; import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks'; -import { SectionHeader } from '@/components/common/SectionHeader'; + import { ROLES } from '@/lib/constants'; import { Users } from '@/components/icons'; import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton'; @@ -10,6 +10,8 @@ import { TeamManage } from './TeamManage'; import { TeamEditForm } from './TeamEditForm'; import { TeamWebsitesDataTable } from './TeamWebsitesDataTable'; import { TeamMembersDataTable } from './TeamMembersDataTable'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Panel } from '@/components/common/Panel'; export function TeamDetails({ teamId }: { teamId: string }) { const team = useContext(TeamContext); @@ -33,30 +35,32 @@ export function TeamDetails({ teamId }: { teamId: string }) { user.role !== ROLES.viewOnly); return ( - - }> + + }> {!isTeamOwner && !isAdmin && } - - setTab(value)}> - - {formatMessage(labels.details)} - {formatMessage(labels.members)} - {formatMessage(labels.websites)} - {isTeamOwner && {formatMessage(labels.manage)}} - - - - - - - - - - - - - - + + + setTab(value)}> + + {formatMessage(labels.details)} + {formatMessage(labels.members)} + {formatMessage(labels.websites)} + {isTeamOwner && {formatMessage(labels.manage)}} + + + + + + + + + + + + + + + ); } diff --git a/src/app/(main)/settings/teams/[teamId]/TeamEditForm.tsx b/src/app/(main)/settings/teams/[teamId]/TeamEditForm.tsx index 9c053ac8..ce206169 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamEditForm.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamEditForm.tsx @@ -10,7 +10,7 @@ import { 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 { TeamContext } from '@/app/(main)/settings/teams/[teamId]/TeamProvider'; const generateId = () => `team_${getRandomChars(16)}`; diff --git a/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx b/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx index c8056146..a9216439 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx @@ -1,6 +1,7 @@ import { useMessages, useModified } from '@/components/hooks'; -import { Row, Button, Icon, Modal, DialogTrigger, Dialog, useToast } from '@umami/react-zen'; +import { Dialog, useToast } from '@umami/react-zen'; import { TeamMemberEditForm } from './TeamMemberEditForm'; +import { ActionButton } from '@/components/input/ActionButton'; import { Edit } from '@/components/icons'; export function TeamMemberEditButton({ @@ -25,27 +26,18 @@ export function TeamMemberEditButton({ }; return ( - - - - - {({ close }) => ( - - )} - - - + }> + + {({ close }) => ( + + )} + + ); } diff --git a/src/app/(main)/settings/teams/[teamId]/TeamMemberRemoveButton.tsx b/src/app/(main)/settings/teams/[teamId]/TeamMemberRemoveButton.tsx index be1a11cc..5b1d323f 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamMemberRemoveButton.tsx @@ -2,7 +2,8 @@ import { ConfirmationForm } from '@/components/common/ConfirmationForm'; import { useApi, useMessages, useModified } from '@/components/hooks'; import { messages } from '@/components/messages'; import { Trash } from '@/components/icons'; -import { Button, Icon, Modal, DialogTrigger, Dialog } from '@umami/react-zen'; +import { Dialog } from '@umami/react-zen'; +import { ActionButton } from '@/components/input/ActionButton'; export function TeamMemberRemoveButton({ teamId, @@ -34,29 +35,22 @@ export function TeamMemberRemoveButton({ }; return ( - - - - - {({ close }) => ( - - )} - - - + }> + + {({ close }) => ( + + )} + + ); } diff --git a/src/app/(main)/teams/[teamId]/TeamProvider.tsx b/src/app/(main)/settings/teams/[teamId]/TeamProvider.tsx similarity index 100% rename from src/app/(main)/teams/[teamId]/TeamProvider.tsx rename to src/app/(main)/settings/teams/[teamId]/TeamProvider.tsx diff --git a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx index 88ec9b40..d172c3aa 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)/settings/teams/[teamId]/TeamProvider'; import { TeamDetails } from '@/app/(main)/settings/teams/[teamId]/TeamDetails'; export function TeamSettingsPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/settings/teams/[teamId]/TeamWebsitesTable.tsx b/src/app/(main)/settings/teams/[teamId]/TeamWebsitesTable.tsx index f04f1f85..da97fc51 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamWebsitesTable.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamWebsitesTable.tsx @@ -1,58 +1,19 @@ -import { DataColumn, DataTable, Icon, MenuItem, Text, Row } from '@umami/react-zen'; -import { useLoginQuery, useMessages } from '@/components/hooks'; -import { Eye, Edit } from '@/components/icons'; -import { MenuButton } from '@/components/input/MenuButton'; +import { DataColumn, DataTable } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; import Link from 'next/link'; -export function TeamWebsitesTable({ - teamId, - data = [], - allowEdit = false, -}: { - teamId: string; - data: any[]; - allowEdit?: boolean; -}) { - const { user } = useLoginQuery(); +export function TeamWebsitesTable({ teamId, data = [] }: { teamId: string; data: any[] }) { const { formatMessage, labels } = useMessages(); return ( - {(row: any) => {row.name}} + {(row: any) => {row.name}} {(row: any) => row?.createUser?.username} - - {(row: any) => { - const { id: websiteId } = row; - - return ( - - - - - - - {formatMessage(labels.view)} - - - {allowEdit && (teamId || user?.isAdmin) && ( - - - - - - {formatMessage(labels.edit)} - - - )} - - ); - }} - ); } diff --git a/src/app/(main)/settings/websites/WebsitesTable.tsx b/src/app/(main)/settings/websites/WebsitesTable.tsx index a7948781..2d285c75 100644 --- a/src/app/(main)/settings/websites/WebsitesTable.tsx +++ b/src/app/(main)/settings/websites/WebsitesTable.tsx @@ -57,7 +57,7 @@ export function WebsitesTable({ )} {allowEdit && ( - + diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx index fce82f50..1c1f4a9e 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx @@ -1,5 +1,4 @@ import { Button, Modal, DialogTrigger, Dialog, Column } from '@umami/react-zen'; -import { useRouter } from 'next/navigation'; import { useLoginQuery, useMessages, @@ -17,9 +16,9 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: const { formatMessage, labels, messages } = useMessages(); const { user } = useLoginQuery(); const { touch } = useModified(); - const { teamId, renderUrl } = useNavigation(); - const router = useRouter(); + const { router, pathname, teamId, renderUrl } = useNavigation(); const { data: teams } = useUserTeamsQuery(user.id); + const isAdmin = pathname.startsWith('/admin'); const canTransferWebsite = ( @@ -49,21 +48,23 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: return ( - - - - - - {({ close }) => ( - - )} - - - - + {!isAdmin && ( + + + + + + {({ close }) => ( + + )} + + + + + )} - }> - - - - - {formatMessage(labels.view)} - - - - - {formatMessage(labels.details)} - {formatMessage(labels.trackingCode)} - {formatMessage(labels.shareUrl)} - {formatMessage(labels.manage)} - - - - - - - - - - - - - - - + + + {formatMessage(labels.details)} + {formatMessage(labels.trackingCode)} + {formatMessage(labels.shareUrl)} + {formatMessage(labels.manage)} + + + + + + + + + + + + + + ); } diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsHeader.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsHeader.tsx new file mode 100644 index 00000000..ee542f2f --- /dev/null +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsHeader.tsx @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { PageHeader } from '@/components/common/PageHeader'; +import { Globe } from '@/components/icons'; + +export function WebsiteSettingsHeader() { + const website = useContext(WebsiteContext); + + return } />; +} diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 5df7a819..1aff4e7e 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,11 +1,19 @@ 'use client'; +import { Column } from '@umami/react-zen'; import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import { WebsiteSettings } from './WebsiteSettings'; +import { WebsiteSettingsHeader } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsHeader'; +import { Panel } from '@/components/common/Panel'; export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { return ( - + + + + + + ); } diff --git a/src/app/(main)/teams/[teamId]/layout.tsx b/src/app/(main)/teams/[teamId]/layout.tsx deleted file mode 100644 index 1dfdd243..00000000 --- a/src/app/(main)/teams/[teamId]/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TeamProvider } from './TeamProvider'; -import { Metadata } from 'next'; - -export default async function ({ - children, - params, -}: { - children: any; - params: Promise<{ teamId: string }>; -}) { - const { teamId } = await params; - - return {children}; -} - -export const metadata: Metadata = { - title: 'Teams', -}; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index a3442d1c..f8617225 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen'; import { ListFilter } from '@/components/icons'; -import { FilterEditForm } from '@/components/common/FilterEditForm'; +import { FilterEditForm } from '@/components/input/FilterEditForm'; import { useMessages, useNavigation, useFilters } from '@/components/hooks'; import { filtersArrayToObject } from '@/lib/params'; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index ae2cf1bb..1935fe07 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -5,11 +5,17 @@ import { Share, Edit } from '@/components/icons'; import { Favicon } from '@/components/common/Favicon'; import { ActiveUsers } from '@/components/metrics/ActiveUsers'; import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm'; -import { useMessages } from '@/components/hooks'; +import { useMessages, useNavigation } from '@/components/hooks'; import { LinkButton } from '@/components/common/LinkButton'; export function WebsiteHeader() { const website = useWebsite(); + const { renderUrl, pathname } = useNavigation(); + const isSettings = pathname.endsWith('/settings'); + + if (isSettings) { + return null; + } return ( } marginBottom="3"> @@ -17,7 +23,7 @@ export function WebsiteHeader() { - + diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index 14374904..b095afdb 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -1,17 +1,23 @@ 'use client'; import { ReactNode } from 'react'; -import { Column } from '@umami/react-zen'; +import { Column, Grid } from '@umami/react-zen'; import { WebsiteProvider } from './WebsiteProvider'; import { PageBody } from '@/components/common/PageBody'; import { WebsiteHeader } from './WebsiteHeader'; +import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { return ( - - - {children} - + + + + + + + {children} + + ); } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 9ba78662..54cfe0bb 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -1,4 +1,3 @@ -import { Icon, Text, Row, NavMenu, NavMenuItem, NavMenuGroup, Column } from '@umami/react-zen'; import { Eye, Lightning, @@ -16,14 +15,16 @@ import { ChartPie, } from '@/components/icons'; import { useMessages, useNavigation } from '@/components/hooks'; -import Link from 'next/link'; +import { SideMenu } from '@/components/common/SideMenu'; import { WebsiteSelect } from '@/components/input/WebsiteSelect'; export function WebsiteNav({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { pathname, renderUrl, teamId } = useNavigation(); - const links = [ + const renderPath = (path: string) => renderUrl(`/websites/${websiteId}${path}`); + + const items = [ { label: formatMessage(labels.traffic), items: [ @@ -31,25 +32,31 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { id: 'overview', label: formatMessage(labels.overview), icon: , - path: '', + path: renderPath(''), }, { id: 'events', label: formatMessage(labels.events), icon: , - path: '/events', + path: renderPath('/events'), }, { id: 'sessions', label: formatMessage(labels.sessions), icon: , - path: '/sessions', + path: renderPath('/sessions'), }, { id: 'realtime', label: formatMessage(labels.realtime), icon: , - path: '/realtime', + path: renderPath('/realtime'), + }, + { + id: 'breakdown', + label: formatMessage(labels.breakdown), + icon: , + path: renderPath('/breakdown'), }, ], }, @@ -60,48 +67,42 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { id: 'goals', label: formatMessage(labels.goals), icon: , - path: '/goals', + path: renderPath('/goals'), }, { id: 'funnel', label: formatMessage(labels.funnels), icon: , - path: '/funnels', + path: renderPath('/funnels'), }, { id: 'journeys', label: formatMessage(labels.journeys), icon: , - path: '/journeys', + path: renderPath('/journeys'), }, { id: 'retention', label: formatMessage(labels.retention), icon: , - path: '/retention', + path: renderPath('/retention'), }, ], }, { label: formatMessage(labels.segments), items: [ - { - id: 'breakdown', - label: formatMessage(labels.breakdown), - icon: , - path: '/breakdown', - }, { id: 'segments', label: formatMessage(labels.segments), icon: , - path: '/segments', + path: renderPath('/segments'), }, { id: 'cohorts', label: formatMessage(labels.cohorts), icon: , - path: '/cohorts', + path: renderPath('/cohorts'), }, ], }, @@ -112,53 +113,31 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { id: 'utm', label: formatMessage(labels.utm), icon: , - path: '/utm', + path: renderPath('/utm'), }, { id: 'revenue', label: formatMessage(labels.revenue), icon: , - path: '/revenue', + path: renderPath('/revenue'), }, { id: 'attribution', label: formatMessage(labels.attribution), icon: , - path: '/attribution', + path: renderPath('/attribution'), }, ], }, ]; - const selected = - links.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path))?.id || + const selectedKey = + items.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path))?.id || 'overview'; return ( - + - - {links.map(({ label, items }) => { - return ( - - {items.map(({ id, label, icon, path }) => { - const isSelected = selected === id; - - return ( - - - - {icon} - {label} - - - - ); - })} - - ); - })} - - + ); } diff --git a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx index d93c7d8c..805f1252 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx @@ -3,7 +3,7 @@ import { useMessages, useWebsiteEventsQuery } from '@/components/hooks'; import { EventsTable } from './EventsTable'; import { DataGrid } from '@/components/common/DataGrid'; import { ReactNode } from 'react'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; export function EventsDataTable({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index 8e965528..a706242a 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -1,6 +1,6 @@ import { useFormat } from '@/components//hooks/useFormat'; import { Empty } from '@/components/common/Empty'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; import { useCountryNames, useLocale, useMessages, useTimezone } from '@/components/hooks'; import { Eye, Visitor, Bolt } from '@/components/icons'; import { BROWSERS, OS_NAMES } from '@/lib/constants'; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx index e6b493bf..15e53a6b 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx @@ -6,7 +6,7 @@ import { ListTable } from '@/components/metrics/ListTable'; import { useMessages } from '@/components/hooks'; import { RealtimeData } from '@/lib/types'; import { WebsiteContext } from '../WebsiteProvider'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; const FILTER_REFERRERS = 'filter-referrers'; const FILTER_PAGES = 'filter-pages'; diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx index 3c198fd4..d8539406 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx @@ -1,7 +1,7 @@ import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { Plus } from '@/components/icons'; -import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; +import { SegmentEditForm } from './SegmentEditForm'; export function SegmentAddButton({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx new file mode 100644 index 00000000..fc86e78f --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx @@ -0,0 +1,55 @@ +import { Dialog } from '@umami/react-zen'; +import { ActionButton } from '@/components/input/ActionButton'; +import { Trash } from '@/components/icons'; +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { messages } from '@/components/messages'; +import { useApi, useMessages, useModified } from '@/components/hooks'; + +export function SegmentDeleteButton({ + segmentId, + websiteId, + name, + onSave, +}: { + segmentId: string; + websiteId: string; + name: string; + onSave?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { del, useMutation } = useApi(); + const { mutate, isPending, error } = useMutation({ + mutationFn: () => del(`/websites/${websiteId}/segments/${segmentId}`), + }); + const { touch } = useModified(); + + const handleConfirm = (close: () => void) => { + mutate(null, { + onSuccess: () => { + touch('segments'); + onSave?.(); + close(); + }, + }); + }; + + return ( + }> + + {({ close }) => ( + + )} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx new file mode 100644 index 00000000..c7041f7a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx @@ -0,0 +1,34 @@ +import { ActionButton } from '@/components/input/ActionButton'; +import { Edit } from '@/components/icons'; +import { Dialog } from '@umami/react-zen'; +import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; +import { useMessages } from '@/components/hooks'; + +export function SegmentEditButton({ + segmentId, + websiteId, + filters, +}: { + segmentId: string; + websiteId: string; + filters: any[]; +}) { + const { formatMessage, labels } = useMessages(); + + return ( + }> + + {({ close }) => { + return ( + + ); + }} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx index 15133e8e..b19785a5 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx @@ -6,24 +6,30 @@ import { FormSubmitButton, TextField, Label, + Loading, } from '@umami/react-zen'; import { subMonths, endOfDay } from 'date-fns'; import { FieldFilters } from '@/components/input/FieldFilters'; import { useState } from 'react'; -import { useApi, useMessages, useModified } from '@/components/hooks'; +import { useApi, useMessages, useModified, useWebsiteSegmentQuery } from '@/components/hooks'; import { filtersArrayToObject } from '@/lib/params'; export function SegmentEditForm({ + segmentId, websiteId, filters = [], + showFilters = true, onSave, onClose, }: { + segmentId: string; websiteId: string; filters?: any[]; + showFilters?: boolean; onSave?: () => void; onClose?: () => void; }) { + const { data } = useWebsiteSegmentQuery(websiteId, segmentId); const { formatMessage, labels } = useMessages(); const [currentFilters, setCurrentFilters] = useState(filters); const { touch } = useModified(); @@ -33,7 +39,10 @@ export function SegmentEditForm({ const { post, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: (data: any) => - post(`/websites/${websiteId}/segments`, { ...data, type: 'segment' }), + post(`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, { + ...data, + type: 'segment', + }), }); const handleSubmit = async (data: any) => { @@ -49,28 +58,40 @@ export function SegmentEditForm({ ); }; + if (segmentId && !data) { + return ; + } + return ( -
+ - + - - + {showFilters && ( + <> + + + + )} - + {formatMessage(labels.save)} diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx index 0231db87..fe8317ce 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx @@ -1,12 +1,14 @@ -import { DataTable, DataColumn, Icon, Row, Text, MenuItem } from '@umami/react-zen'; -import { useMessages } from '@/components/hooks'; +import { DataTable, DataColumn, Row } from '@umami/react-zen'; +import { useMessages, useNavigation } from '@/components/hooks'; import { Empty } from '@/components/common/Empty'; -import { Edit, Trash } from '@/components/icons'; import { DateDistance } from '@/components/common/DateDistance'; -import { MenuButton } from '@/components/input/MenuButton'; +import { filtersObjectToArray } from '@/lib/params'; +import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton'; +import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton'; export function SegmentsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); + const { websiteId } = useNavigation(); if (data.length === 0) { return ; @@ -20,27 +22,17 @@ export function SegmentsTable({ data = [] }) { {(row: any) => { - const { id } = row; + const { id, name, parameters } = row; return ( - - - - - - - {formatMessage(labels.edit)} - - - - - - - - {formatMessage(labels.delete)} - - - + + + + ); }} diff --git a/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx b/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx new file mode 100644 index 00000000..84bd0f4e --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx @@ -0,0 +1,25 @@ +'use client'; +import { Column, Icon, Row, Text } from '@umami/react-zen'; +import { WebsiteSettingsPage } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage'; +import { LinkButton } from '@/components/common/LinkButton'; +import { Arrow } from '@/components/icons'; +import { useNavigation } from '@/components/hooks'; + +export function SettingsPage({ websiteId }: { websiteId: string }) { + const { pathname } = useNavigation(); + return ( + + + + + + + + Back + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/page.tsx b/src/app/(main)/websites/[websiteId]/settings/page.tsx new file mode 100644 index 00000000..a3aed621 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/page.tsx @@ -0,0 +1,12 @@ +import { SettingsPage } from './SettingsPage'; +import { Metadata } from 'next'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Settings', +}; diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 9f3365ad..92f8af0c 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -27,7 +27,7 @@ export function PageHeader({ {...props} > - {icon && {icon}} + {icon && {icon}} {title && {title}} {description && {description}} diff --git a/src/components/common/SideMenu.tsx b/src/components/common/SideMenu.tsx index ad167116..4daa4909 100644 --- a/src/components/common/SideMenu.tsx +++ b/src/components/common/SideMenu.tsx @@ -1,27 +1,68 @@ -import { ReactNode } from 'react'; -import { Text, NavMenu, NavMenuItem, Icon, Row } from '@umami/react-zen'; +import { + Text, + Heading, + NavMenu, + NavMenuItem, + Icon, + Row, + Column, + NavMenuGroup, +} from '@umami/react-zen'; import Link from 'next/link'; export interface SideMenuProps { - items: { id: string; label: string; url: string; icon?: ReactNode }[]; + items: { label: string; items: { id: string; label: string; icon?: any; path: string }[] }[]; + title?: string; selectedKey?: string; + allowMinimize?: boolean; } -export function SideMenu({ items, selectedKey }: SideMenuProps) { +export function SideMenu({ items, title, selectedKey, allowMinimize, children }: SideMenuProps) { return ( - - {items.map(({ id, label, url, icon }) => { - return ( - - - - {icon && {icon}} - {label} - - - - ); - })} - + + {children} + {title && ( + + {title} + + )} + + {items.map(({ label, items }) => { + return ( + + {items.map(({ id, label, icon, path }) => { + const isSelected = selectedKey === id; + + return ( + + + + {icon} + {label} + + + + ); + })} + + ); + })} + + ); } diff --git a/src/components/icons.ts b/src/components/icons.ts index 95094acb..c25aa003 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -35,6 +35,7 @@ export { Plus, RefreshCw as Refresh, Settings, + Settings2 as Knobs, Share, Sheet, Slash, diff --git a/src/components/input/ActionButton.tsx b/src/components/input/ActionButton.tsx new file mode 100644 index 00000000..7798aa48 --- /dev/null +++ b/src/components/input/ActionButton.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from 'react'; +import { Button, Icon, Modal, DialogTrigger, TooltipTrigger, Tooltip } from '@umami/react-zen'; + +export function ActionButton({ + onClick, + icon, + tooltip, + children, +}: { + onSave?: () => void; + icon?: ReactNode; + tooltip?: string; + children?: React.ReactNode; +}) { + return ( + + + + {tooltip} + + {children} + + ); +} diff --git a/src/components/input/DeleteButton.tsx b/src/components/input/DeleteButton.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index 445db323..3ce30294 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -1,4 +1,14 @@ -import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen'; +import { + Button, + Icon, + Text, + Row, + TooltipTrigger, + Tooltip, + Modal, + Dialog, + DialogTrigger, +} from '@umami/react-zen'; import { useNavigation, useMessages, @@ -6,8 +16,9 @@ import { useFilters, useWebsiteSegmentQuery, } from '@/components/hooks'; -import { Close } from '@/components/icons'; +import { Close, Bookmark } from '@/components/icons'; import { isSearchOperator } from '@/lib/params'; +import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; export function FilterBar({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); @@ -65,16 +76,37 @@ export function FilterBar({ websiteId }: { websiteId: string }) { ); })} - - - - {formatMessage(labels.clearAll)} - - + + + + + + {formatMessage(labels.saveSegment)} + + + + + {({ close }) => { + return ; + }} + + + + + + + {formatMessage(labels.clearAll)} + + + ); } diff --git a/src/components/common/FilterButtons.tsx b/src/components/input/FilterButtons.tsx similarity index 100% rename from src/components/common/FilterButtons.tsx rename to src/components/input/FilterButtons.tsx diff --git a/src/components/common/FilterEditForm.tsx b/src/components/input/FilterEditForm.tsx similarity index 96% rename from src/components/common/FilterEditForm.tsx rename to src/components/input/FilterEditForm.tsx index b253b780..33846575 100644 --- a/src/components/common/FilterEditForm.tsx +++ b/src/components/input/FilterEditForm.tsx @@ -57,7 +57,7 @@ export function FilterEditForm({ onSave={setCurrentFilters} /> - + - + diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index e30557f1..5b890095 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -9,6 +9,7 @@ import { MenuSeparator, MenuSection, Text, + Row, } from '@umami/react-zen'; import { useRouter } from 'next/navigation'; import { useMessages, useLoginQuery } from '@/components/hooks'; @@ -36,25 +37,31 @@ export function ProfileButton() { - - - - {formatMessage(labels.settings)} + + + + + {formatMessage(labels.settings)} + {user.isAdmin && ( - - - - {formatMessage(labels.admin)} + + + + + {formatMessage(labels.admin)} + )} {!cloudMode && ( - - - - {formatMessage(labels.logout)} + + + + + {formatMessage(labels.logout)} + )} diff --git a/src/components/input/SegmentFilters.tsx b/src/components/input/SegmentFilters.tsx index 3cf8ce46..1406d7a3 100644 --- a/src/components/input/SegmentFilters.tsx +++ b/src/components/input/SegmentFilters.tsx @@ -1,4 +1,4 @@ -import { List, Column, ListItem } from '@umami/react-zen'; +import { List, ListItem } from '@umami/react-zen'; import { useWebsiteSegmentsQuery } from '@/components/hooks'; import { LoadingPanel } from '@/components/common/LoadingPanel'; @@ -9,25 +9,23 @@ export interface SegmentFiltersProps { } export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) { - const { data, isLoading } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' }); + const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' }); const handleChange = (id: string) => { onSave?.(id); }; return ( - - - handleChange(id[0])}> - {data?.data?.map(item => { - return ( - - {item.name} - - ); - })} - - - + + handleChange(id[0])}> + {data?.data?.map(item => { + return ( + + {item.name} + + ); + })} + + ); } diff --git a/src/components/input/SegmentSaveButton.tsx b/src/components/input/SegmentSaveButton.tsx new file mode 100644 index 00000000..f6cee0e1 --- /dev/null +++ b/src/components/input/SegmentSaveButton.tsx @@ -0,0 +1,26 @@ +import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm'; + +export function SegmentSaveButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + {({ close }) => { + return ; + }} + + + + ); +} diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx index d9fd5e08..6c84493a 100644 --- a/src/components/input/TeamsButton.tsx +++ b/src/components/input/TeamsButton.tsx @@ -15,7 +15,7 @@ import { Pressable, } from '@umami/react-zen'; import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks'; -import { Chevron, User, Users } from '@/components/icons'; +import { Chevron, User, Users, LogOut } from '@/components/icons'; export function TeamsButton({ showText = true }: { showText?: boolean }) { const { user } = useLoginQuery(); @@ -81,6 +81,17 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) { ))} + + + + + + + + {formatMessage(labels.logout)} + + + diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 18a1e4a7..40e31a2f 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -33,13 +33,13 @@ export function WebsiteSelect({ items={data?.['data'] || []} value={websiteId} isLoading={isLoading} - buttonProps={buttonProps} + buttonProps={{ ...buttonProps }} allowSearch={true} searchValue={search} onSearch={handleSearch} onChange={handleSelect} renderValue={() => ( - + {website?.name} )} diff --git a/src/components/messages.ts b/src/components/messages.ts index e0e8413e..c06f4a02 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -342,6 +342,10 @@ export const labels = defineMessages({ traffic: { id: 'label.traffic', defaultMessage: 'Traffic' }, behavior: { id: 'label.behavior', defaultMessage: 'Behavior' }, growth: { id: 'label.growth', defaultMessage: 'Growth' }, + account: { id: 'label.account', defaultMessage: 'Account' }, + application: { id: 'label.application', defaultMessage: 'Application' }, + saveSegment: { id: 'label.save-segment', defaultMessage: 'Save segment' }, + saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save cohort' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index 5fd43fef..086d3dc7 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -1,5 +1,5 @@ import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; import { FilterLink } from '@/components/common/FilterLink'; import { useMessages, useNavigation } from '@/components/hooks'; import { emptyFilter } from '@/lib/filters'; diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx index e6d85495..e98f9134 100644 --- a/src/components/metrics/QueryParametersTable.tsx +++ b/src/components/metrics/QueryParametersTable.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Row, Text } from '@umami/react-zen'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; import { emptyFilter, paramFilter } from '@/lib/filters'; import { MetricsTable, MetricsTableProps } from './MetricsTable'; import { useMessages } from '@/components/hooks'; diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index 0b3f1ee3..8990db1a 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -1,7 +1,7 @@ import { Row } from '@umami/react-zen'; import { FilterLink } from '@/components/common/FilterLink'; import { Favicon } from '@/components/common/Favicon'; -import { FilterButtons } from '@/components/common/FilterButtons'; +import { FilterButtons } from '@/components/input/FilterButtons'; import { useMessages, useNavigation } from '@/components/hooks'; import { MetricsTable, MetricsTableProps } from './MetricsTable'; import thenby from 'thenby'; diff --git a/src/index.ts b/src/index.ts index 243d5ea7..0f89b6fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export * from '@/app/(main)/settings/teams/TeamAddForm'; export * from '@/app/(main)/settings/teams/TeamJoinForm'; export * from '@/app/(main)/settings/teams/TeamLeaveButton'; export * from '@/app/(main)/settings/teams/TeamLeaveForm'; +export * from '@/app/(main)/settings/teams/[teamId]/TeamProvider'; export * from '@/app/(main)/settings/teams/TeamsAddButton'; export * from '@/app/(main)/settings/teams/TeamsDataTable'; export * from '@/app/(main)/settings/teams/TeamsHeader'; @@ -37,7 +38,6 @@ export * from '@/app/(main)/settings/websites/WebsitesDataTable'; export * from '@/app/(main)/settings/websites/WebsitesHeader'; export * from '@/app/(main)/settings/websites/WebsitesTable'; -export * from '@/app/(main)/teams/[teamId]/TeamProvider'; export * from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; export * from '@/components/common/ConfirmationForm'; @@ -46,7 +46,7 @@ export * from '@/components/common/Empty'; export * from '@/components/common/ErrorBoundary'; export * from '@/components/common/ErrorMessage'; export * from '@/components/common/Favicon'; -export * from '@/components/common/FilterButtons'; +export * from '@/components/input/FilterButtons'; export * from '@/components/common/FilterLink'; export * from '@/components/common/HamburgerButton'; export * from '@/components/common/LinkButton';