diff --git a/package.json b/package.json index 2f5b27dde..8e35c620a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "type": "module", "scripts": { - "dev": "next dev -p 3001 --turbopack", + "dev": "next dev -p 3001 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx index 3e59043ca..4e43c7607 100644 --- a/src/app/(main)/links/[linkId]/LinkControls.tsx +++ b/src/app/(main)/links/[linkId]/LinkControls.tsx @@ -1,8 +1,8 @@ import { Column, Row } from '@umami/react-zen'; -import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; +import { MonthFilter } from '@/components/input/MonthFilter'; import { ExportButton } from '@/components/input/ExportButton'; export function LinkControls({ @@ -24,7 +24,7 @@ export function LinkControls({ {allowFilter ? :
} {allowDateFilter && } {allowDownload && } - {allowMonthFilter && } + {allowMonthFilter && } {allowFilter && } diff --git a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx index 5e9d6cbfd..43c14050f 100644 --- a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx +++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx @@ -12,10 +12,9 @@ export function LinkMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { dateRange } = useDateRange(linkId); + const { isAllTime } = useDateRange(); const { formatMessage, labels } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); - const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx index e0e10213b..10cdaf8d1 100644 --- a/src/app/(main)/links/[linkId]/LinkPage.tsx +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -7,9 +7,30 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels'; -import { Column, Grid } from '@umami/react-zen'; +import { Column, Dialog, Grid, Modal } from '@umami/react-zen'; +import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView'; +import { useNavigation } from '@/components/hooks'; + +const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event']; export function LinkPage({ linkId }: { linkId: string }) { + const { + router, + query: { view }, + updateParams, + } = useNavigation(); + + const handleClose = (close: () => void) => { + router.push(updateParams({ view: undefined })); + close(); + }; + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + router.push(updateParams({ view: undefined })); + } + }; + return ( @@ -23,6 +44,19 @@ export function LinkPage({ linkId }: { linkId: string }) { + + + {({ close }) => { + return ( + handleClose(close)} + /> + ); + }} + + diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx index c5fee534f..33f49222b 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx @@ -1,8 +1,8 @@ import { Column, Row } from '@umami/react-zen'; -import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; +import { MonthFilter } from '@/components/input/MonthFilter'; import { ExportButton } from '@/components/input/ExportButton'; export function PixelControls({ @@ -24,7 +24,7 @@ export function PixelControls({ {allowFilter ? :
} {allowDateFilter && } {allowDownload && } - {allowMonthFilter && } + {allowMonthFilter && } {allowFilter && } diff --git a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx index 5b01ef842..0305df7f3 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx @@ -12,10 +12,9 @@ export function PixelMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { dateRange } = useDateRange(pixelId); + const { isAllTime } = useDateRange(); const { formatMessage, labels } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId); - const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx index a65c821e0..be462bfd4 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx @@ -7,9 +7,30 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls'; import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels'; -import { Column, Grid } from '@umami/react-zen'; +import { Column, Dialog, Grid, Modal } from '@umami/react-zen'; +import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView'; +import { useNavigation } from '@/components/hooks'; + +const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event']; export function PixelPage({ pixelId }: { pixelId: string }) { + const { + router, + query: { view }, + updateParams, + } = useNavigation(); + + const handleClose = (close: () => void) => { + router.push(updateParams({ view: undefined })); + close(); + }; + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + router.push(updateParams({ view: undefined })); + } + }; + return ( @@ -23,6 +44,19 @@ export function PixelPage({ pixelId }: { pixelId: string }) { + + + {({ close }) => { + return ( + handleClose(close)} + /> + ); + }} + + diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 18f2a625c..d5c129eec 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,5 +1,4 @@ 'use client'; -import { Column } from '@umami/react-zen'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings'; import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader'; @@ -7,10 +6,8 @@ import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/setting export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { return ( - - - - + + ); } diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index 3f781cb7c..d6d743906 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,15 +1,13 @@ import { ReactNode } from 'react'; -import { Row, Text, Icon, DataTable, DataColumn, MenuItem } from '@umami/react-zen'; +import { Icon, DataTable, DataColumn } from '@umami/react-zen'; +import { LinkButton } from '@/components/common/LinkButton'; import { useMessages, useNavigation } from '@/components/hooks'; -import { MenuButton } from '@/components/input/MenuButton'; -import { Eye, SquarePen } from '@/components/icons'; +import { SquarePen } from '@/components/icons'; import { Empty } from '@/components/common/Empty'; export function WebsitesTable({ data = [], showActions, - allowEdit, - allowView, renderLink, }: { data: Record[]; @@ -37,28 +35,11 @@ export function WebsitesTable({ const websiteId = row.id; return ( - - {allowView && ( - - - - - - {formatMessage(labels.view)} - - - )} - {allowEdit && ( - - - - - - {formatMessage(labels.edit)} - - - )} - + + + + + ); }} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx index 5b375bded..497913b78 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx @@ -12,7 +12,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx index d54797412..eff072fde 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx @@ -1,26 +1,23 @@ 'use client'; import { useState } from 'react'; -import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen'; +import { Button, Column, Box, DialogTrigger, Popover, Dialog, IconLabel } from '@umami/react-zen'; import { useDateRange, useMessages } from '@/components/hooks'; import { ListCheck } from '@/components/icons'; import { Panel } from '@/components/common/Panel'; import { Breakdown } from './Breakdown'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm'; -import { SectionHeader } from '@/components/common/SectionHeader'; export function BreakdownPage({ websiteId }: { websiteId: string }) { const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); const [fields, setFields] = useState(['path']); return ( - - - + { return ( - - + {({ close }) => ( )} - + ); diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx index 1918aadf5..c155662ff 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx @@ -12,7 +12,7 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) { const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' }); const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx index 30f92325f..6cd417288 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx @@ -12,7 +12,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) { const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' }); const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx index d75828222..269279625 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx @@ -13,7 +13,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); const [steps, setSteps] = useState(DEFAULT_STEP); const [startStep, setStartStep] = useState(''); const [endStep, setEndStep] = useState(''); diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx index 0d74c38e9..ed5120620 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx @@ -7,7 +7,7 @@ import { useDateRange } from '@/components/hooks'; export function RevenuePage({ websiteId }: { websiteId: string }) { const { dateRange: { startDate, endDate, unit }, - } = useDateRange(websiteId); + } = useDateRange(); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx index a5999a7c9..30b9bff22 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx @@ -7,7 +7,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro export function UTMPage({ websiteId }: { websiteId: string }) { const { dateRange: { startDate, endDate }, - } = useDateRange(websiteId); + } = useDateRange(); return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 08e0c1c83..2406cde24 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -11,7 +11,7 @@ export function WebsiteChart({ websiteId: string; compareMode?: boolean; }) { - const { dateRange, dateCompare } = useDateRange(websiteId); + const { dateRange, dateCompare } = useDateRange(); const { startDate, endDate, unit, value } = dateRange; const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index 97be18214..6b03ef6e0 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -1,8 +1,8 @@ import { Column, Row } from '@umami/react-zen'; -import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; +import { MonthFilter } from '@/components/input/MonthFilter'; import { ExportButton } from '@/components/input/ExportButton'; export function WebsiteControls({ @@ -26,7 +26,7 @@ export function WebsiteControls({ {allowFilter ? :
} {allowDateFilter && } {allowDownload && } - {allowMonthFilter && } + {allowMonthFilter && } {allowFilter && } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx index 9fac4c8e7..b90a6da07 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx @@ -26,9 +26,11 @@ import { Lightning } from '@/components/svg'; export function WebsiteExpandedView({ websiteId, + excludedIds = [], onClose, }: { websiteId: string; + excludedIds?: string[]; onClose?: () => void; }) { const { formatMessage, labels } = useMessages(); @@ -37,9 +39,11 @@ export function WebsiteExpandedView({ query: { view }, } = useNavigation(); + const filterExcluded = (item: { id: string }) => !excludedIds.includes(item.id); + const items = [ { - label: formatMessage(labels.pages), + label: 'URL', items: [ { id: 'path', @@ -71,7 +75,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'query' }), icon: , }, - ], + ].filter(filterExcluded), }, { label: formatMessage(labels.sources), @@ -94,7 +98,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'domain' }), icon: , }, - ], + ].filter(filterExcluded), }, { label: formatMessage(labels.location), @@ -117,7 +121,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'city' }), icon: , }, - ], + ].filter(filterExcluded), }, { label: formatMessage(labels.environment), @@ -152,7 +156,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'screen' }), icon: , }, - ], + ].filter(filterExcluded), }, { label: formatMessage(labels.other), @@ -175,7 +179,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'tag' }), icon: , }, - ], + ].filter(filterExcluded), }, ]; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index cf75e86a5..e25541e9a 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -2,17 +2,24 @@ import { ReactNode } from 'react'; import { Column, Grid } from '@umami/react-zen'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; +import { useNavigation } from '@/components/hooks'; import { PageBody } from '@/components/common/PageBody'; import { WebsiteHeader } from './WebsiteHeader'; import { WebsiteNav } from './WebsiteNav'; export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { + const { pathname } = useNavigation(); + + const isSettings = pathname.endsWith('/settings'); + return ( - - - - + + {!isSettings && ( + + + + )} {children} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index d217b94f6..7522c8ec0 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -12,10 +12,9 @@ export function WebsiteMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { dateRange } = useDateRange(websiteId); + const { isAllTime } = useDateRange(); const { formatMessage, labels, getErrorMessage } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); - const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {}; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 2ec171b42..3696b7861 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -1,9 +1,18 @@ -import { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons'; -import { Lightning, Path, Money, Compare, Target, Funnel, Magnet, Network } from '@/components/svg'; +import { Text } from '@umami/react-zen'; +import { + Eye, + User, + Clock, + Sheet, + Tag, + ChartPie, + UserPlus, + GitCompareArrows, +} from '@/components/icons'; +import { Lightning, Path, Money, Target, Funnel, Magnet, Network } from '@/components/svg'; import { useMessages, useNavigation } from '@/components/hooks'; import { SideMenu } from '@/components/common/SideMenu'; import { WebsiteSelect } from '@/components/input/WebsiteSelect'; -import { Text } from '@umami/react-zen'; export function WebsiteNav({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); @@ -47,7 +56,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { { id: 'compare', label: formatMessage(labels.compare), - icon: , + icon: , path: renderPath('/compare'), }, { diff --git a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx index 8ed32841e..2c67b76a9 100644 --- a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx @@ -9,7 +9,7 @@ import { useState } from 'react'; export function CompareTables({ websiteId }: { websiteId: string }) { const [data, setData] = useState([]); - const { dateRange, dateCompare } = useDateRange(websiteId); + const { dateRange, dateCompare } = useDateRange(); const { formatMessage, labels } = useMessages(); const { router, diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx index 97a0108f0..216142ec9 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx @@ -1,9 +1,22 @@ +import Link from 'next/link'; import { PageHeader } from '@/components/common/PageHeader'; -import { Globe } from '@/components/icons'; -import { useWebsite } from '@/components/hooks'; +import { Globe, ArrowLeft } from '@/components/icons'; +import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; +import { IconLabel, Row } from '@umami/react-zen'; export function WebsiteSettingsHeader() { const website = useWebsite(); + const { formatMessage, labels } = useMessages(); + const { renderUrl } = useNavigation(); - return } />; + return ( + <> + + + } label={formatMessage(labels.website)} /> + + + } /> + + ); } diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts deleted file mode 100644 index 677e0bd7a..000000000 --- a/src/app/api/users/[userId]/usage/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { z } from 'zod'; -import { json, unauthorized } from '@/lib/response'; -import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website'; -import { getEventUsage } from '@/queries/sql/events/getEventUsage'; -import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage'; -import { parseRequest, getQueryFilters } from '@/lib/request'; - -export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { - const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), - }); - - const { auth, query, error } = await parseRequest(request, schema); - - if (error) { - return error(); - } - - if (!auth.user.isAdmin) { - return unauthorized(); - } - - const { userId } = await params; - const filters = await getQueryFilters(query); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, filters); - const eventDataUsage = await getEventDataUsage(websiteIds, filters); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return json({ - ...usage, - websites: filteredWebsiteUsage, - }); -} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 821b6eaff..776f23b0a 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -4,9 +4,11 @@ import { json, unauthorized } from '@/lib/response'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, searchParams } from '@/lib/schema'; -import { createWebsite } from '@/queries/prisma'; +import { createWebsite, getWebsiteCount } from '@/queries/prisma'; import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; +const CLOUD_WEBSITE_LIMIT = 3; + export async function GET(request: Request) { const schema = z.object({ ...pagingParams, @@ -36,7 +38,7 @@ export async function POST(request: Request) { name: z.string().max(100), domain: z.string().max(500), shareId: z.string().max(50).nullable().optional(), - teamId: z.string().nullable().optional(), + teamId: z.uuid().nullable().optional(), id: z.uuid().nullable().optional(), }); @@ -48,6 +50,14 @@ export async function POST(request: Request) { const { id, name, domain, shareId, teamId } = body; + if (process.env.CLOUD_MODE && !teamId && !auth.user.hasSubscription) { + const count = await getWebsiteCount(auth.user.id); + + if (count >= CLOUD_WEBSITE_LIMIT) { + return unauthorized({ message: 'Website limit reached.' }); + } + } + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { return unauthorized(); } diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx index a93dcc7f8..ce82b54f0 100644 --- a/src/components/common/ConfirmationForm.tsx +++ b/src/components/common/ConfirmationForm.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { Row, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen'; +import { Box, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; export interface ConfirmationFormProps { @@ -25,7 +25,7 @@ export function ConfirmationForm({ return (
- {message} + {message} - - {icon} - {label} - + {label} ); diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 2731e2680..da0b9074e 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -77,7 +77,6 @@ export * from './useModified'; export * from './useNavigation'; export * from './usePagedQuery'; export * from './usePageParameters'; -export * from './useQueryStringDate'; export * from './useRegionNames'; export * from './useSlug'; export * from './useSticky'; diff --git a/src/components/hooks/queries/useDateRangeQuery.ts b/src/components/hooks/queries/useDateRangeQuery.ts index 4af2011cb..32b5cd466 100644 --- a/src/components/hooks/queries/useDateRangeQuery.ts +++ b/src/components/hooks/queries/useDateRangeQuery.ts @@ -1,12 +1,23 @@ import { useApi } from '../useApi'; import { ReactQueryOptions } from '@/lib/types'; +type DateRange = { + startDate?: string; + endDate?: string; +}; + export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - return useQuery({ + + const { data } = useQuery({ queryKey: ['date-range', websiteId], queryFn: () => get(`/websites/${websiteId}/daterange`), enabled: !!websiteId, ...options, }); + + return { + startDate: data?.startDate ? new Date(data.startDate) : null, + endDate: data?.endDate ? new Date(data.endDate) : null, + }; } diff --git a/src/components/hooks/useDateParameters.ts b/src/components/hooks/useDateParameters.ts index ed4a3e40f..359bbc1fd 100644 --- a/src/components/hooks/useDateParameters.ts +++ b/src/components/hooks/useDateParameters.ts @@ -1,10 +1,10 @@ import { useDateRange } from './useDateRange'; import { useTimezone } from './useTimezone'; -export function useDateParameters(websiteId: string) { +export function useDateParameters() { const { dateRange: { startDate, endDate, unit }, - } = useDateRange(websiteId); + } = useDateRange(); const { timezone, toUtc } = useTimezone(); return { diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index dc81cfd7c..0b8db7ce1 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,35 +1,32 @@ -import { getMinimumUnit, parseDateRange } from '@/lib/date'; +import { useNavigation } from '@/components/hooks/useNavigation'; +import { useMemo } from 'react'; +import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date'; import { useLocale } from '@/components/hooks/useLocale'; -import { useApi } from '@/components/hooks//useApi'; -import { useQueryStringDate } from '@/components/hooks/useQueryStringDate'; -import { useGlobalState } from '@/components/hooks/useGlobalState'; +import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants'; -export function useDateRange(websiteId: string) { - const { get } = useApi(); +export function useDateRange(options: { ignoreOffset?: boolean } = {}) { + const { + query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev', all }, + } = useNavigation(); const { locale } = useLocale(); - const { dateRange: defaultDateRange, dateCompare } = useQueryStringDate(); - const [dateRange, setDateRange] = useGlobalState(`date-range:${websiteId}`, defaultDateRange); + const dateRange = useMemo(() => { + const dateRangeObject = parseDateRange(date, locale); - const setDateRangeValue = async (value: string) => { - if (value === 'all') { - const result = await get(`/websites/${websiteId}/daterange`); - const { mindate, maxdate } = result; + return !options.ignoreOffset && offset + ? getOffsetDateRange(dateRangeObject, +offset) + : dateRangeObject; + }, [date, offset, options]); - const startDate = new Date(mindate); - const endDate = new Date(maxdate); - const unit = getMinimumUnit(startDate, endDate); + const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate); - setDateRange({ - startDate, - endDate, - unit, - value, - }); - } else { - setDateRange(parseDateRange(value, locale)); - } + return { + date, + offset, + compare, + isAllTime: !!all, + isCustomRange: date.startsWith('range:'), + dateRange, + dateCompare, }; - - return { dateRange, dateCompare, setDateRange, setDateRangeValue }; } diff --git a/src/components/hooks/useQueryStringDate.ts b/src/components/hooks/useQueryStringDate.ts deleted file mode 100644 index 3b6431a18..000000000 --- a/src/components/hooks/useQueryStringDate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useNavigation } from '@/components/hooks/useNavigation'; -import { useMemo } from 'react'; -import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date'; -import { useLocale } from '@/components/hooks/useLocale'; -import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants'; - -export function useQueryStringDate(options: { ignoreOffset?: boolean } = {}) { - const { - query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev' }, - } = useNavigation(); - const { locale } = useLocale(); - - const dateRange = useMemo(() => { - const dateRangeObject = parseDateRange(date, locale); - - return !options.ignoreOffset && offset - ? getOffsetDateRange(dateRangeObject, +offset) - : dateRangeObject; - }, [date, offset, options]); - - const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate); - - return { date, offset, dateRange, dateCompare }; -} diff --git a/src/components/input/MonthFilter.tsx b/src/components/input/MonthFilter.tsx new file mode 100644 index 000000000..dec64b0f1 --- /dev/null +++ b/src/components/input/MonthFilter.tsx @@ -0,0 +1,18 @@ +import { useDateRange, useNavigation } from '@/components/hooks'; +import { getMonthDateRangeValue } from '@/lib/date'; +import { MonthSelect } from './MonthSelect'; + +export function MonthFilter() { + const { router, updateParams } = useNavigation(); + const { + dateRange: { startDate }, + } = useDateRange(); + + const handleMonthSelect = (date: Date) => { + const range = getMonthDateRangeValue(date); + + router.push(updateParams({ date: range, offset: undefined })); + }; + + return ; +} diff --git a/src/components/input/RefreshButton.tsx b/src/components/input/RefreshButton.tsx index 77096dc09..29b719864 100644 --- a/src/components/input/RefreshButton.tsx +++ b/src/components/input/RefreshButton.tsx @@ -1,7 +1,7 @@ import { LoadingButton, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { setWebsiteDateRange } from '@/store/websites'; import { useDateRange } from '@/components/hooks'; -import { Refresh } from '@/components/icons'; +import { RefreshCw } from '@/components/icons'; import { useMessages } from '@/components/hooks'; export function RefreshButton({ @@ -12,7 +12,7 @@ export function RefreshButton({ isLoading?: boolean; }) { const { formatMessage, labels } = useMessages(); - const { dateRange } = useDateRange(websiteId); + const { dateRange } = useDateRange(); function handleClick() { if (!isLoading && dateRange) { @@ -24,7 +24,7 @@ export function RefreshButton({ - + {formatMessage(labels.refresh)} diff --git a/src/components/input/SegmentFilters.tsx b/src/components/input/SegmentFilters.tsx index 6324d7c4c..d0906d676 100644 --- a/src/components/input/SegmentFilters.tsx +++ b/src/components/input/SegmentFilters.tsx @@ -1,7 +1,8 @@ -import { List, ListItem } from '@umami/react-zen'; +import { IconLabel, List, ListItem } from '@umami/react-zen'; import { useWebsiteSegmentsQuery } from '@/components/hooks'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Empty } from '@/components/common/Empty'; +import { ChartPie, UserPlus } from '@/components/icons'; export interface SegmentFiltersProps { websiteId: string; @@ -29,7 +30,9 @@ export function SegmentFilters({ {data?.data?.map(item => { return ( - {item.name} + : }> + {item.name} + ); })} diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 469da4190..6b59681b1 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -1,10 +1,10 @@ +import { useCallback, useMemo } from 'react'; import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen'; import { isAfter } from 'date-fns'; import { ChevronRight } from '@/components/icons'; -import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; +import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks'; +import { getDateRangeValue } from '@/lib/date'; import { DateFilter } from './DateFilter'; -import { getOffsetDateRange } from '@/lib/date'; -import { useCallback } from 'react'; export interface WebsiteDateFilterProps { websiteId: string; @@ -20,30 +20,33 @@ export function WebsiteDateFilter({ showButtons = true, allowCompare, }: WebsiteDateFilterProps) { - const { dateRange, setDateRange, setDateRangeValue } = useDateRange(websiteId); - const { value, endDate } = dateRange; + const { dateRange, isAllTime, isCustomRange } = useDateRange(); const { formatMessage, labels } = useMessages(); const { router, updateParams, query: { compare = 'prev', offset = 0 }, } = useNavigation(); - const isAllTime = value === 'all'; + const disableForward = isAllTime || isAfter(dateRange.endDate, new Date()); - const isCustomRange = value.startsWith('range'); - - const disableForward = value === 'all' || isAfter(endDate, new Date()); + const websiteDateRange = useDateRangeQuery(websiteId); const handleChange = (date: string) => { - setDateRangeValue(date); - router.push(updateParams({ date, offset: undefined })); + if (date === 'all') { + router.push( + updateParams({ + date: getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate), + offset: undefined, + all: 1, + }), + ); + } else { + router.push(updateParams({ date, offset: undefined, all: undefined })); + } }; const handleIncrement = useCallback( (increment: number) => { - const offsetDate = getOffsetDateRange(dateRange, +offset + increment); - - setDateRange(offsetDate); router.push(updateParams({ offset: +offset + increment })); }, [offset], @@ -53,6 +56,12 @@ export function WebsiteDateFilter({ router.push(updateParams({ compare })); }; + const dateValue = useMemo(() => { + return offset !== 0 + ? getDateRangeValue(dateRange.startDate, dateRange.endDate) + : dateRange.value; + }, [dateRange]); + return ( {showButtons && !isAllTime && !isCustomRange && ( @@ -71,7 +80,7 @@ export function WebsiteDateFilter({ )} {showText && {formatMessage(labels.filter)}} - + {({ close }) => { return ; }} - + ); } diff --git a/src/components/input/WebsiteMonthSelect.tsx b/src/components/input/WebsiteMonthSelect.tsx deleted file mode 100644 index b36aae939..000000000 --- a/src/components/input/WebsiteMonthSelect.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useDateRange } from '@/components/hooks'; -import { dateToRangeValue } from '@/lib/date'; -import { MonthSelect } from './MonthSelect'; - -export function WebsiteMonthSelect({ websiteId }: { websiteId: string }) { - const { - dateRange: { startDate }, - saveDateRange, - } = useDateRange(websiteId); - - const handleMonthSelect = (date: Date) => { - const range = dateToRangeValue(date); - saveDateRange(range); - }; - - return ; -} diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index f2f02eda8..7301faf44 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -14,7 +14,7 @@ export interface EventsChartProps extends BarChartProps { export function EventsChart({ websiteId, focusLabel }: EventsChartProps) { const { dateRange: { startDate, endDate, unit }, - } = useDateRange(websiteId); + } = useDateRange(); const { locale } = useLocale(); const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId); const [label, setLabel] = useState(focusLabel); diff --git a/src/lib/date.ts b/src/lib/date.ts index 2289e4fec..fa87277f3 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -131,14 +131,6 @@ export function parseDateRange(value: string, locale = 'en-US'): DateRange { return null; } - if (value === 'all') { - return { - startDate: new Date(0), - endDate: new Date(1), - value, - }; - } - if (value.startsWith('range')) { const [, startTime, endTime] = value.split(':'); @@ -225,24 +217,28 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) { case 'day': return { ...dateRange, + offset, startDate: addDays(startDate, change), endDate: addDays(endDate, change), }; case 'week': return { ...dateRange, + offset, startDate: addWeeks(startDate, change), endDate: addWeeks(endDate, change), }; case 'month': return { ...dateRange, + offset, startDate: addMonths(startDate, change), endDate: addMonths(endDate, change), }; case 'year': return { ...dateRange, + offset, startDate: addYears(startDate, change), endDate: addYears(endDate, change), }; @@ -250,6 +246,7 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) { return { startDate: add(startDate, change), endDate: add(endDate, change), + offset, value, unit, num, @@ -354,6 +351,10 @@ export function generateTimeSeries( }); } -export function dateToRangeValue(date: Date) { - return `range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`; +export function getDateRangeValue(startDate: Date, endDate: Date) { + return `range:${startDate.getTime()}:${endDate.getTime()}`; +} + +export function getMonthDateRangeValue(date: Date) { + return getDateRangeValue(startOfMonth(date), endOfMonth(date)); } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 35f716be3..4bc581a74 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -284,7 +284,7 @@ function getClient() { replicaUrl: process.env.DATABASE_REPLICA_URL, }); - if (process.env.NODE_ENV !== 'production') { + if (!globalThis[PRISMA]) { globalThis[PRISMA] = prisma.client; } diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index b1a762b90..315dca038 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -203,3 +203,11 @@ export async function deleteWebsite(websiteId: string) { return data; }); } + +export async function getWebsiteCount(userId: string) { + return prisma.client.website.count({ + where: { + userId, + }, + }); +} diff --git a/src/queries/sql/getWebsiteDateRange.ts b/src/queries/sql/getWebsiteDateRange.ts index 4c0d21f0f..2f7f9f190 100644 --- a/src/queries/sql/getWebsiteDateRange.ts +++ b/src/queries/sql/getWebsiteDateRange.ts @@ -20,8 +20,8 @@ async function relationalQuery(websiteId: string) { const result = await rawQuery( ` select - min(created_at) as mindate, - max(created_at) as maxdate + min(created_at) as startDate, + max(created_at) as endDate from website_event where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} @@ -42,8 +42,8 @@ async function clickhouseQuery(websiteId: string) { const result = await rawQuery( ` select - min(created_at) as mindate, - max(created_at) as maxdate + min(created_at) as startDate, + max(created_at) as endDate from website_event_stats_hourly where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64}