diff --git a/next.config.ts b/next.config.ts index 278e7a6d..a4ba1c36 100644 --- a/next.config.ts +++ b/next.config.ts @@ -97,12 +97,7 @@ const headers = [ }, ]; -const rewrites = [ - { - source: '/teams/:id/settings/:path*', - destination: '/settings/:path*', - }, -]; +const rewrites = []; if (trackerScriptURL) { rewrites.push({ @@ -134,6 +129,11 @@ const redirects = [ destination: '/teams/:id/websites', permanent: true, }, + { + source: '/teams/:id/settings', + destination: '/teams/:id/settings/preferences', + permanent: true, + }, { source: '/admin', destination: '/admin/users', @@ -205,7 +205,7 @@ export default { destination: '/api/scripts/telemetry', }, { - source: '/teams/:teamId/:path((?!settings).*)*', + source: '/teams/:teamId/:path*', destination: '/:path*', }, ]; diff --git a/package.json b/package.json index 9f41eded..e62c41ec 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,6 @@ "prisma": "6.14.0", "pure-rand": "^7.0.1", "react": "^19.1.1", - "react-basics": "^0.126.0", "react-dom": "^19.1.1", "react-error-boundary": "^4.0.4", "react-intl": "^7.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5da37b3..c00d5f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,9 +158,6 @@ importers: react: specifier: ^19.1.1 version: 19.1.1 - react-basics: - specifier: ^0.126.0 - version: 0.126.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) @@ -6048,13 +6045,6 @@ packages: 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-basics@0.126.0: - resolution: {integrity: sha512-TQtNZMeH5FtJjYxSN72rBmZWlIcs9jK3oVSCUUxfZq9LnFdoFSagTLCrihs3YCnX8vZEJXaJHQsp7lKEfyH5sw==} - engines: {node: '>= 14'} - peerDependencies: - react: ^18.2.0 - react-dom: ^18.2.0 - react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -13881,16 +13871,6 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - react-basics@0.126.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): - dependencies: - '@react-spring/web': 9.7.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - classnames: 2.5.1 - date-fns: 2.30.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-hook-form: 7.62.0(react@19.1.1) - react-window: 1.8.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 diff --git a/prisma/migrations/14_add_link_and_pixel/migration.sql b/prisma/migrations/14_add_link_and_pixel/migration.sql index 9c08fc61..4feec4f4 100644 --- a/prisma/migrations/14_add_link_and_pixel/migration.sql +++ b/prisma/migrations/14_add_link_and_pixel/migration.sql @@ -12,7 +12,7 @@ CREATE TABLE "link" ( "link_id" UUID NOT NULL, "name" VARCHAR(100) NOT NULL, "url" VARCHAR(500) NOT NULL, - "slug" VARCHAR(100) NOT NULL, + "slug" VARCHAR(100) UNIQUE NOT NULL, "user_id" UUID, "team_id" UUID, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, @@ -26,7 +26,7 @@ CREATE TABLE "link" ( CREATE TABLE "pixel" ( "pixel_id" UUID NOT NULL, "name" VARCHAR(100) NOT NULL, - "slug" VARCHAR(100) NOT NULL, + "slug" VARCHAR(100) UNIQUE NOT NULL, "user_id" UUID, "team_id" UUID, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0434393b..7c45d5ff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -280,7 +280,7 @@ model Link { id String @id() @unique() @map("link_id") @db.Uuid name String @db.VarChar(100) url String @db.VarChar(500) - slug String @db.VarChar(100) + slug String @unique() @db.VarChar(100) userId String? @map("user_id") @db.Uuid teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) @@ -300,7 +300,7 @@ model Link { model Pixel { id String @id() @unique() @map("pixel_id") @db.Uuid name String @db.VarChar(100) - slug String @db.VarChar(100) + slug String @unique() @db.VarChar(100) userId String? @map("user_id") @db.Uuid teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index 895c48cc..e360dae1 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -6,6 +6,7 @@ import { SidebarHeader, Row, SidebarProps, + ThemeButton, } from '@umami/react-zen'; import { Globe, @@ -14,24 +15,19 @@ import { Logo, Grid2X2, Settings, - LockKeyhole, PanelLeft, } from '@/components/icons'; import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; import { TeamsButton } from '@/components/input/TeamsButton'; import { PanelButton } from '@/components/input/PanelButton'; +import { ProfileButton } from '@/components/input/ProfileButton'; export function SideNav(props: SidebarProps) { const { formatMessage, labels } = useMessages(); const { pathname, renderUrl, websiteId } = useNavigation(); const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed'); - const hasNav = !!( - websiteId || - pathname.startsWith('/admin') || - pathname.startsWith('/settings') || - pathname.endsWith('/settings') - ); + const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings')); const links = [ { @@ -64,15 +60,9 @@ export function SideNav(props: SidebarProps) { { id: 'settings', label: formatMessage(labels.settings), - path: '/settings', + path: renderUrl('/settings'), icon: , }, - { - id: 'admin', - label: formatMessage(labels.admin), - path: '/admin', - icon: , - }, ]; return ( @@ -99,10 +89,18 @@ export function SideNav(props: SidebarProps) { {bottomLinks.map(({ id, path, label, icon }) => { return ( - + ); })} + + + {!isCollapsed && !hasNav && ( + + + + )} + diff --git a/src/app/(main)/admin/AdminLayout.tsx b/src/app/(main)/admin/AdminLayout.tsx index 87abd77b..a33b745a 100644 --- a/src/app/(main)/admin/AdminLayout.tsx +++ b/src/app/(main)/admin/AdminLayout.tsx @@ -17,7 +17,7 @@ export function AdminLayout({ children }: { children: ReactNode }) { const items = [ { - label: formatMessage(labels.application), + label: formatMessage(labels.manage), items: [ { id: 'users', diff --git a/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx b/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx index 07104c52..a7851d78 100644 --- a/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx +++ b/src/app/(main)/admin/teams/[teamId]/AdminTeamPage.tsx @@ -1,11 +1,11 @@ 'use client'; -import { TeamDetails } from '@/app/(main)/teams/[teamId]/TeamDetails'; +import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings'; import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider'; export function AdminTeamPage({ teamId }: { teamId: string }) { return ( - + ); } diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx index 9617049f..b3cf7df0 100644 --- a/src/app/(main)/settings/SettingsLayout.tsx +++ b/src/app/(main)/settings/SettingsLayout.tsx @@ -20,12 +20,6 @@ export function SettingsLayout({ children }: { children: ReactNode }) { path: renderUrl('/settings/preferences'), icon: , }, - { - id: 'teams', - label: formatMessage(labels.teams), - path: renderUrl('/settings/teams'), - icon: , - }, ], }, { @@ -37,12 +31,18 @@ export function SettingsLayout({ children }: { children: ReactNode }) { path: renderUrl('/settings/profile'), icon: , }, + { + id: 'teams', + label: formatMessage(labels.teams), + path: renderUrl('/settings/teams'), + icon: , + }, ], }, ]; const selectedKey = - items.flatMap(e => e.items)?.find(({ path }) => path && pathname.endsWith(path))?.id || + items.flatMap(e => e.items)?.find(({ path }) => path && pathname.includes(path))?.id || 'overview'; return ( diff --git a/src/app/(main)/settings/preferences/PreferencesPage.tsx b/src/app/(main)/settings/preferences/PreferencesPage.tsx index 99289956..4ddd3511 100644 --- a/src/app/(main)/settings/preferences/PreferencesPage.tsx +++ b/src/app/(main)/settings/preferences/PreferencesPage.tsx @@ -4,16 +4,19 @@ import { useMessages } from '@/components/hooks'; import { Panel } from '@/components/common/Panel'; import { PreferenceSettings } from './PreferenceSettings'; import { PageHeader } from '@/components/common/PageHeader'; +import { PageBody } from '@/components/common/PageBody'; 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 2d1ce0d3..dab836de 100644 --- a/src/app/(main)/settings/profile/ProfilePage.tsx +++ b/src/app/(main)/settings/profile/ProfilePage.tsx @@ -4,16 +4,19 @@ import { useMessages } from '@/components/hooks'; import { Panel } from '@/components/common/Panel'; import { Column } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; +import { PageBody } from '@/components/common/PageBody'; export function ProfilePage() { const { formatMessage, labels } = useMessages(); return ( - - - - - - + + + + + + + + ); } diff --git a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx index 4efa4ecd..3736299d 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx @@ -1,11 +1,11 @@ 'use client'; import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider'; -import { TeamDetails } from '@/app/(main)/teams/[teamId]/TeamDetails'; +import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings'; export function TeamSettingsPage({ teamId }: { teamId: string }) { return ( - + ); } diff --git a/src/app/(main)/teams/TeamsPage.tsx b/src/app/(main)/teams/TeamsPage.tsx new file mode 100644 index 00000000..3b0f57ea --- /dev/null +++ b/src/app/(main)/teams/TeamsPage.tsx @@ -0,0 +1,19 @@ +'use client'; +import { TeamsDataTable } from '@/app/(main)/teams/TeamsDataTable'; +import { TeamsHeader } from '@/app/(main)/teams/TeamsHeader'; +import { Column } from '@umami/react-zen'; +import { Panel } from '@/components/common/Panel'; +import { PageBody } from '@/components/common/PageBody'; + +export function TeamsPage() { + return ( + + + + + + + + + ); +} diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx index 7d6e890a..d77ede11 100644 --- a/src/app/(main)/teams/TeamsTable.tsx +++ b/src/app/(main)/teams/TeamsTable.tsx @@ -1,5 +1,5 @@ import { DataColumn, DataTable, Icon, MenuItem, Text, Row } from '@umami/react-zen'; -import { useMessages } from '@/components/hooks'; +import { useMessages, useNavigation } from '@/components/hooks'; import { Eye, Edit } from '@/components/icons'; import { ROLES } from '@/lib/constants'; import { MenuButton } from '@/components/input/MenuButton'; @@ -14,20 +14,21 @@ export function TeamsTable({ showActions?: boolean; }) { const { formatMessage, labels } = useMessages(); + const { renderUrl } = useNavigation(); return ( - {(row: any) => {row.name}} + {(row: any) => {row.name}} - {(row: any) => row.users.find(({ role }) => role === ROLES.teamOwner)?.user?.username} + {(row: any) => row?.members?.find(({ role }) => role === ROLES.teamOwner)?.user?.username} - {(row: any) => row._count.websites} + {(row: any) => row?._count?.websites} - {(row: any) => row._count.users} + {(row: any) => row?._count?.members} {showActions ? ( diff --git a/src/app/(main)/teams/[teamId]/TeamDetails.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx similarity index 92% rename from src/app/(main)/teams/[teamId]/TeamDetails.tsx rename to src/app/(main)/teams/[teamId]/TeamSettings.tsx index 7a923f01..acf1a936 100644 --- a/src/app/(main)/teams/[teamId]/TeamDetails.tsx +++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx @@ -13,7 +13,7 @@ import { TeamMembersDataTable } from './TeamMembersDataTable'; import { PageHeader } from '@/components/common/PageHeader'; import { Panel } from '@/components/common/Panel'; -export function TeamDetails({ teamId }: { teamId: string }) { +export function TeamSettings({ teamId }: { teamId: string }) { const team = useContext(TeamContext); const { formatMessage, labels } = useMessages(); const { user } = useLoginQuery(); @@ -23,12 +23,12 @@ export function TeamDetails({ teamId }: { teamId: string }) { const isAdmin = pathname.includes('/admin'); const isTeamOwner = - !!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) && + !!team?.members?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) && user.role !== ROLES.viewOnly; const canEdit = user.isAdmin || - (!!team?.teamUser?.find( + (!!team?.members?.find( ({ userId, role }) => (role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id, ) && diff --git a/src/app/(main)/teams/page.tsx b/src/app/(main)/teams/page.tsx new file mode 100644 index 00000000..ce353895 --- /dev/null +++ b/src/app/(main)/teams/page.tsx @@ -0,0 +1,10 @@ +import { Metadata } from 'next'; +import { TeamsPage } from './TeamsPage'; + +export default function () { + return ; +} + +export const metadata: Metadata = { + title: 'Teams', +}; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 74a9479b..e2a6bc36 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -137,7 +137,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { return ( - + ); } diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index 73208c7d..b276da6e 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -43,7 +43,7 @@ export function LoadingPanel({ {!error && !isLoading && !isFetching && empty && renderEmpty()} {/* Show main content when data exists */} - {!error && !empty && children} + {!isLoading && !isFetching && !error && !empty && children} ); } diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index 5b890095..9e1cbd3f 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -1,4 +1,4 @@ -import { Key } from 'react'; +import { Fragment } from 'react'; import { Icon, Button, @@ -11,19 +11,37 @@ import { Text, Row, } from '@umami/react-zen'; -import { useRouter } from 'next/navigation'; -import { useMessages, useLoginQuery } from '@/components/hooks'; -import { LogOut, Settings, UserCircle, LockKeyhole } from '@/components/icons'; +import { useMessages, useLoginQuery, useNavigation } from '@/components/hooks'; +import { LogOut, UserCircle, LockKeyhole } from '@/components/icons'; export function ProfileButton() { const { formatMessage, labels } = useMessages(); const { user } = useLoginQuery(); - const router = useRouter(); + const { renderUrl } = useNavigation(); const cloudMode = !!process.env.cloudMode; - const handleSelect = (key: Key) => { - router.push(`/${key}`); - }; + const items = [ + { + id: 'profile', + label: formatMessage(labels.profile), + path: renderUrl('/settings/profile'), + icon: , + }, + user.isAdmin && + !cloudMode && { + id: 'admin', + label: formatMessage(labels.admin), + path: '/admin', + icon: , + }, + { + id: 'LogOut', + label: formatMessage(labels.logout), + path: '/logout', + icon: , + separator: true, + }, + ].filter(n => n); return ( @@ -33,37 +51,22 @@ export function ProfileButton() { - + - - - - - - {formatMessage(labels.settings)} - - - {user.isAdmin && ( - - - - - - {formatMessage(labels.admin)} - - - )} - {!cloudMode && ( - - - - - - {formatMessage(labels.logout)} - - - )} + {items.map(({ id, path, label, icon, separator }) => { + return ( + + {separator && } + + + {icon} + {label} + + + + ); + })} diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 40e31a2f..917c7d21 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -1,17 +1,14 @@ import { useState } from 'react'; import { Select, SelectProps, ListItem, Text } from '@umami/react-zen'; import { useUserWebsitesQuery, useWebsiteQuery, useNavigation } from '@/components/hooks'; -import { ButtonProps } from 'react-basics'; export function WebsiteSelect({ websiteId, teamId, - buttonProps, ...props }: { websiteId?: string; teamId?: string; - buttonProps?: ButtonProps; } & SelectProps) { const { router, renderUrl } = useNavigation(); const [search, setSearch] = useState(''); @@ -33,7 +30,7 @@ export function WebsiteSelect({ items={data?.['data'] || []} value={websiteId} isLoading={isLoading} - buttonProps={{ ...buttonProps }} + buttonProps={{ variant: 'outline' }} allowSearch={true} searchValue={search} onSearch={handleSearch} diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 2f88b415..1db956ca 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -117,7 +117,7 @@ export function MetricsTable({ return ( - + {allowSearch && } diff --git a/src/index.ts b/src/index.ts index 6a8ebecf..0c84624c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export * from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton'; export * from '@/app/(main)/teams/[teamId]/TeamMembersDataTable'; export * from '@/app/(main)/teams/[teamId]/TeamMembersTable'; export * from '@/app/(main)/teams/[teamId]/TeamDeleteForm'; -export * from '@/app/(main)/teams/[teamId]/TeamDetails'; +export * from '@/app/(main)/teams/[teamId]/TeamSettings'; export * from '@/app/(main)/teams/[teamId]/TeamEditForm'; export * from '@/app/(main)/teams/[teamId]/TeamManage'; export * from '@/app/(main)/teams/[teamId]/TeamWebsiteRemoveButton'; diff --git a/src/queries/prisma/link.ts b/src/queries/prisma/link.ts index f7f5e158..ea46023c 100644 --- a/src/queries/prisma/link.ts +++ b/src/queries/prisma/link.ts @@ -2,7 +2,7 @@ import { Prisma, Link } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; import { PageResult, QueryFilters } from '@/lib/types'; -async function findLink(criteria: Prisma.LinkFindUniqueArgs): Promise { +export async function findLink(criteria: Prisma.LinkFindUniqueArgs): Promise { return prisma.client.link.findUnique(criteria); } diff --git a/src/queries/prisma/pixel.ts b/src/queries/prisma/pixel.ts index db494fa8..a68c5300 100644 --- a/src/queries/prisma/pixel.ts +++ b/src/queries/prisma/pixel.ts @@ -2,7 +2,7 @@ import { Prisma, Pixel } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; import { PageResult, QueryFilters } from '@/lib/types'; -async function findPixel(criteria: Prisma.PixelFindUniqueArgs): Promise { +export async function findPixel(criteria: Prisma.PixelFindUniqueArgs): Promise { return prisma.client.pixel.findUnique(criteria); } @@ -22,7 +22,7 @@ export async function getPixels( const where: Prisma.PixelWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(search, [{ name: 'contains' }]), + ...prisma.getSearchParameters(search, [{ name: 'contains' }, { slug: 'contains' }]), }; return prisma.pagedQuery('pixel', { ...criteria, where }, filters); diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index 5bdf6862..683aab0f 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -4,7 +4,7 @@ import prisma from '@/lib/prisma'; import { PageResult, QueryFilters } from '@/lib/types'; import { ROLES } from '@/lib/constants'; -async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise { +export async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise { return prisma.client.website.findUnique(criteria); } diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 92056206..c351b95c 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -12,6 +12,7 @@ export interface SaveEventArgs { sessionId: string; visitId: string; createdAt?: Date; + eventType?: number; // Page pageTitle?: string; @@ -66,6 +67,7 @@ async function relationalQuery({ sessionId, visitId, createdAt, + eventType, pageTitle, tag, hostname, @@ -113,7 +115,7 @@ async function relationalQuery({ ttclid, lifatid, twclid, - eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, + eventType: eventType || (eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView), eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag, hostname,