diff --git a/package.json b/package.json index 8e35c620a..2f5b27dde 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "type": "module", "scripts": { - "dev": "next dev -p 3001 --turbo", + "dev": "next dev -p 3001 --turbopack", "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 4e43c7607..3e59043ca 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 '@/components/input/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { MonthFilter } from '@/components/input/MonthFilter'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; 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 43c14050f..5e9d6cbfd 100644 --- a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx +++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx @@ -12,9 +12,10 @@ export function LinkMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { isAllTime } = useDateRange(); + const { dateRange } = useDateRange(linkId); const { formatMessage, labels } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); + const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx index 10cdaf8d1..e0e10213b 100644 --- a/src/app/(main)/links/[linkId]/LinkPage.tsx +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -7,30 +7,9 @@ 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, 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']; +import { Column, Grid } from '@umami/react-zen'; 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 ( @@ -44,19 +23,6 @@ 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 33f49222b..c5fee534f 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 '@/components/input/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { MonthFilter } from '@/components/input/MonthFilter'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; 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 0305df7f3..5b01ef842 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx @@ -12,9 +12,10 @@ export function PixelMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { isAllTime } = useDateRange(); + const { dateRange } = useDateRange(pixelId); const { formatMessage, labels } = useMessages(); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId); + const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx index be462bfd4..a65c821e0 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx @@ -7,30 +7,9 @@ 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, 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']; +import { Column, Grid } from '@umami/react-zen'; 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 ( @@ -44,19 +23,6 @@ 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 d5c129eec..18f2a625c 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,4 +1,5 @@ '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'; @@ -6,8 +7,10 @@ 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 d6d743906..3f781cb7c 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,13 +1,15 @@ import { ReactNode } from 'react'; -import { Icon, DataTable, DataColumn } from '@umami/react-zen'; -import { LinkButton } from '@/components/common/LinkButton'; +import { Row, Text, Icon, DataTable, DataColumn, MenuItem } from '@umami/react-zen'; import { useMessages, useNavigation } from '@/components/hooks'; -import { SquarePen } from '@/components/icons'; +import { MenuButton } from '@/components/input/MenuButton'; +import { Eye, SquarePen } from '@/components/icons'; import { Empty } from '@/components/common/Empty'; export function WebsitesTable({ data = [], showActions, + allowEdit, + allowView, renderLink, }: { data: Record[]; @@ -35,11 +37,28 @@ 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 497913b78..5b375bded 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(); + } = useDateRange(websiteId); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx index eff072fde..d54797412 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx @@ -1,23 +1,26 @@ 'use client'; import { useState } from 'react'; -import { Button, Column, Box, DialogTrigger, Popover, Dialog, IconLabel } from '@umami/react-zen'; +import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } 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(); + } = useDateRange(websiteId); 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 c155662ff..1918aadf5 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(); + } = useDateRange(websiteId); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx index 6cd417288..30f92325f 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(); + } = useDateRange(websiteId); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx index 269279625..d75828222 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(); + } = useDateRange(websiteId); 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 ed5120620..0d74c38e9 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(); + } = useDateRange(websiteId); return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx index 30b9bff22..a5999a7c9 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(); + } = useDateRange(websiteId); return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 2406cde24..08e0c1c83 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(); + const { dateRange, dateCompare } = useDateRange(websiteId); 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 6b03ef6e0..97be18214 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 '@/components/input/WebsiteFilterButton'; +import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; -import { MonthFilter } from '@/components/input/MonthFilter'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; 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 b90a6da07..9fac4c8e7 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx @@ -26,11 +26,9 @@ import { Lightning } from '@/components/svg'; export function WebsiteExpandedView({ websiteId, - excludedIds = [], onClose, }: { websiteId: string; - excludedIds?: string[]; onClose?: () => void; }) { const { formatMessage, labels } = useMessages(); @@ -39,11 +37,9 @@ export function WebsiteExpandedView({ query: { view }, } = useNavigation(); - const filterExcluded = (item: { id: string }) => !excludedIds.includes(item.id); - const items = [ { - label: 'URL', + label: formatMessage(labels.pages), items: [ { id: 'path', @@ -75,7 +71,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'query' }), icon: , }, - ].filter(filterExcluded), + ], }, { label: formatMessage(labels.sources), @@ -98,7 +94,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'domain' }), icon: , }, - ].filter(filterExcluded), + ], }, { label: formatMessage(labels.location), @@ -121,7 +117,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'city' }), icon: , }, - ].filter(filterExcluded), + ], }, { label: formatMessage(labels.environment), @@ -156,7 +152,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'screen' }), icon: , }, - ].filter(filterExcluded), + ], }, { label: formatMessage(labels.other), @@ -179,7 +175,7 @@ export function WebsiteExpandedView({ path: updateParams({ view: 'tag' }), icon: , }, - ].filter(filterExcluded), + ], }, ]; diff --git a/src/components/input/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx similarity index 89% rename from src/components/input/WebsiteFilterButton.tsx rename to src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index 09fe84538..723c0249b 100644 --- a/src/components/input/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -1,4 +1,4 @@ -import { Button, Icon, DialogTrigger, Dialog, Popover, Text } from '@umami/react-zen'; +import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen'; import { ListFilter } from '@/components/icons'; import { FilterEditForm } from '@/components/input/FilterEditForm'; import { useMessages, useNavigation } from '@/components/hooks'; @@ -32,13 +32,13 @@ export function WebsiteFilterButton({ {showText && {formatMessage(labels.filter)}} - + {({ close }) => { return ; }} - + ); } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index e25541e9a..cf75e86a5 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -2,24 +2,17 @@ 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 7522c8ec0..d217b94f6 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -12,9 +12,10 @@ export function WebsiteMetricsBar({ showChange?: boolean; compareMode?: boolean; }) { - const { isAllTime } = useDateRange(); + const { dateRange } = useDateRange(websiteId); 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 3696b7861..2ec171b42 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -1,18 +1,9 @@ -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 { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons'; +import { Lightning, Path, Money, Compare, 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(); @@ -56,7 +47,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 2c67b76a9..8ed32841e 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(); + const { dateRange, dateCompare } = useDateRange(websiteId); 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 216142ec9..97a0108f0 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx @@ -1,22 +1,9 @@ -import Link from 'next/link'; import { PageHeader } from '@/components/common/PageHeader'; -import { Globe, ArrowLeft } from '@/components/icons'; -import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; -import { IconLabel, Row } from '@umami/react-zen'; +import { Globe } from '@/components/icons'; +import { useWebsite } from '@/components/hooks'; export function WebsiteSettingsHeader() { const website = useWebsite(); - const { formatMessage, labels } = useMessages(); - const { renderUrl } = useNavigation(); - return ( - <> - - - } label={formatMessage(labels.website)} /> - - - } /> - - ); + return } />; } diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts new file mode 100644 index 000000000..677e0bd7a --- /dev/null +++ b/src/app/api/users/[userId]/usage/route.ts @@ -0,0 +1,60 @@ +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 776f23b0a..821b6eaff 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -4,11 +4,9 @@ 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, getWebsiteCount } from '@/queries/prisma'; +import { createWebsite } 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, @@ -38,7 +36,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.uuid().nullable().optional(), + teamId: z.string().nullable().optional(), id: z.uuid().nullable().optional(), }); @@ -50,14 +48,6 @@ 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 ce82b54f0..a93dcc7f8 100644 --- a/src/components/common/ConfirmationForm.tsx +++ b/src/components/common/ConfirmationForm.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { Box, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen'; +import { Row, 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} - {label} + + {icon} + {label} + ); diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index da0b9074e..2731e2680 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -77,6 +77,7 @@ 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 32b5cd466..4af2011cb 100644 --- a/src/components/hooks/queries/useDateRangeQuery.ts +++ b/src/components/hooks/queries/useDateRangeQuery.ts @@ -1,23 +1,12 @@ 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(); - - const { data } = useQuery({ + return 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 359bbc1fd..ed4a3e40f 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() { +export function useDateParameters(websiteId: string) { const { dateRange: { startDate, endDate, unit }, - } = useDateRange(); + } = useDateRange(websiteId); const { timezone, toUtc } = useTimezone(); return { diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 0b8db7ce1..dc81cfd7c 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,32 +1,35 @@ -import { useNavigation } from '@/components/hooks/useNavigation'; -import { useMemo } from 'react'; -import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date'; +import { getMinimumUnit, parseDateRange } from '@/lib/date'; import { useLocale } from '@/components/hooks/useLocale'; -import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants'; +import { useApi } from '@/components/hooks//useApi'; +import { useQueryStringDate } from '@/components/hooks/useQueryStringDate'; +import { useGlobalState } from '@/components/hooks/useGlobalState'; -export function useDateRange(options: { ignoreOffset?: boolean } = {}) { - const { - query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev', all }, - } = useNavigation(); +export function useDateRange(websiteId: string) { + const { get } = useApi(); const { locale } = useLocale(); + const { dateRange: defaultDateRange, dateCompare } = useQueryStringDate(); - const dateRange = useMemo(() => { - const dateRangeObject = parseDateRange(date, locale); + const [dateRange, setDateRange] = useGlobalState(`date-range:${websiteId}`, defaultDateRange); - return !options.ignoreOffset && offset - ? getOffsetDateRange(dateRangeObject, +offset) - : dateRangeObject; - }, [date, offset, options]); + const setDateRangeValue = async (value: string) => { + if (value === 'all') { + const result = await get(`/websites/${websiteId}/daterange`); + const { mindate, maxdate } = result; - const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate); + const startDate = new Date(mindate); + const endDate = new Date(maxdate); + const unit = getMinimumUnit(startDate, endDate); - return { - date, - offset, - compare, - isAllTime: !!all, - isCustomRange: date.startsWith('range:'), - dateRange, - dateCompare, + setDateRange({ + startDate, + endDate, + unit, + value, + }); + } else { + setDateRange(parseDateRange(value, locale)); + } }; + + return { dateRange, dateCompare, setDateRange, setDateRangeValue }; } diff --git a/src/components/hooks/useQueryStringDate.ts b/src/components/hooks/useQueryStringDate.ts new file mode 100644 index 000000000..3b6431a18 --- /dev/null +++ b/src/components/hooks/useQueryStringDate.ts @@ -0,0 +1,24 @@ +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 deleted file mode 100644 index dec64b0f1..000000000 --- a/src/components/input/MonthFilter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 29b719864..77096dc09 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 { RefreshCw } from '@/components/icons'; +import { Refresh } 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(); + const { dateRange } = useDateRange(websiteId); 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 d0906d676..6324d7c4c 100644 --- a/src/components/input/SegmentFilters.tsx +++ b/src/components/input/SegmentFilters.tsx @@ -1,8 +1,7 @@ -import { IconLabel, List, ListItem } from '@umami/react-zen'; +import { 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; @@ -30,9 +29,7 @@ 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 6b59681b1..469da4190 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, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks'; -import { getDateRangeValue } from '@/lib/date'; +import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { DateFilter } from './DateFilter'; +import { getOffsetDateRange } from '@/lib/date'; +import { useCallback } from 'react'; export interface WebsiteDateFilterProps { websiteId: string; @@ -20,33 +20,30 @@ export function WebsiteDateFilter({ showButtons = true, allowCompare, }: WebsiteDateFilterProps) { - const { dateRange, isAllTime, isCustomRange } = useDateRange(); + const { dateRange, setDateRange, setDateRangeValue } = useDateRange(websiteId); + const { value, endDate } = dateRange; const { formatMessage, labels } = useMessages(); const { router, updateParams, query: { compare = 'prev', offset = 0 }, } = useNavigation(); - const disableForward = isAllTime || isAfter(dateRange.endDate, new Date()); + const isAllTime = value === 'all'; - const websiteDateRange = useDateRangeQuery(websiteId); + const isCustomRange = value.startsWith('range'); + + const disableForward = value === 'all' || isAfter(endDate, new Date()); const handleChange = (date: string) => { - 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 })); - } + setDateRangeValue(date); + router.push(updateParams({ date, offset: undefined })); }; const handleIncrement = useCallback( (increment: number) => { + const offsetDate = getOffsetDateRange(dateRange, +offset + increment); + + setDateRange(offsetDate); router.push(updateParams({ offset: +offset + increment })); }, [offset], @@ -56,12 +53,6 @@ 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 && ( @@ -80,7 +71,7 @@ export function WebsiteDateFilter({ )} { + const range = dateToRangeValue(date); + saveDateRange(range); + }; + + return ; +} diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index 7301faf44..f2f02eda8 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(); + } = useDateRange(websiteId); 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 fa87277f3..2289e4fec 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -131,6 +131,14 @@ 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(':'); @@ -217,28 +225,24 @@ 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), }; @@ -246,7 +250,6 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) { return { startDate: add(startDate, change), endDate: add(endDate, change), - offset, value, unit, num, @@ -351,10 +354,6 @@ export function generateTimeSeries( }); } -export function getDateRangeValue(startDate: Date, endDate: Date) { - return `range:${startDate.getTime()}:${endDate.getTime()}`; -} - -export function getMonthDateRangeValue(date: Date) { - return getDateRangeValue(startOfMonth(date), endOfMonth(date)); +export function dateToRangeValue(date: Date) { + return `range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`; } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 4bc581a74..35f716be3 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -284,7 +284,7 @@ function getClient() { replicaUrl: process.env.DATABASE_REPLICA_URL, }); - if (!globalThis[PRISMA]) { + if (process.env.NODE_ENV !== 'production') { globalThis[PRISMA] = prisma.client; } diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index 315dca038..b1a762b90 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -203,11 +203,3 @@ 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 2f7f9f190..4c0d21f0f 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 startDate, - max(created_at) as endDate + min(created_at) as mindate, + max(created_at) as maxdate 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 startDate, - max(created_at) as endDate + min(created_at) as mindate, + max(created_at) as maxdate from website_event_stats_hourly where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64}