From 3f167e05ba4bd15f8e17f559416f5389adbc4832 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 23 Aug 2025 22:21:25 -0700 Subject: [PATCH] New compare page. --- .../(main)/links/[linkId]/LinkControls.tsx | 10 +- .../(main)/pixels/[pixelId]/PixelControls.tsx | 10 +- .../[websiteId]/(reports)/utm/UTMPage.tsx | 2 +- .../websites/[websiteId]/WebsiteChart.tsx | 2 +- .../websites/[websiteId]/WebsiteControls.tsx | 4 +- .../websites/[websiteId]/WebsiteNav.tsx | 14 ++- .../websites/[websiteId]/WebsitePage.tsx | 2 +- .../[websiteId]/compare/ComparePage.tsx | 20 +++ .../CompareTables.tsx} | 116 ++++++++++-------- .../websites/[websiteId]/compare/page.tsx | 12 ++ .../[websiteId]/events/EventsPage.tsx | 2 +- .../[websiteId]/segments/SegmentEditForm.tsx | 20 +-- src/components/hooks/useFields.ts | 3 +- src/components/input/FilterBar.tsx | 5 +- src/components/input/WebsiteDateFilter.tsx | 45 ++----- src/components/metrics/MetricsTable.tsx | 10 +- src/lib/request.ts | 1 + 17 files changed, 151 insertions(+), 127 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx rename src/app/(main)/websites/[websiteId]/{WebsiteCompareTables.tsx => compare/CompareTables.tsx} (58%) create mode 100644 src/app/(main)/websites/[websiteId]/compare/page.tsx diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx index 9b58b8d9..3e59043c 100644 --- a/src/app/(main)/links/[linkId]/LinkControls.tsx +++ b/src/app/(main)/links/[linkId]/LinkControls.tsx @@ -10,12 +10,10 @@ export function LinkControls({ allowFilter = true, allowDateFilter = true, allowMonthFilter, - allowCompare, allowDownload = false, }: { linkId: string; allowFilter?: boolean; - allowCompare?: boolean; allowDateFilter?: boolean; allowMonthFilter?: boolean; allowDownload?: boolean; @@ -24,13 +22,7 @@ export function LinkControls({ {allowFilter ? :
} - {allowDateFilter && ( - - )} + {allowDateFilter && } {allowDownload && } {allowMonthFilter && } diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx index 9d46378f..c5fee534 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx @@ -10,12 +10,10 @@ export function PixelControls({ allowFilter = true, allowDateFilter = true, allowMonthFilter, - allowCompare, allowDownload = false, }: { pixelId: string; allowFilter?: boolean; - allowCompare?: boolean; allowDateFilter?: boolean; allowMonthFilter?: boolean; allowDownload?: boolean; @@ -24,13 +22,7 @@ export function PixelControls({ {allowFilter ? :
} - {allowDateFilter && ( - - )} + {allowDateFilter && } {allowDownload && } {allowMonthFilter && } diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx index 084b73b7..a5999a7c 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx @@ -11,7 +11,7 @@ export function UTMPage({ websiteId }: { websiteId: string }) { return ( - + ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 743b4f62..b72f00a0 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -9,7 +9,7 @@ export function WebsiteChart({ compareMode, }: { websiteId: string; - compareMode?: string; + compareMode?: boolean; }) { const { dateRange, dateCompare } = useDateRange(websiteId); const { startDate, endDate, unit, value } = dateRange; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index f0471ed6..97be1821 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -10,15 +10,15 @@ export function WebsiteControls({ allowFilter = true, allowDateFilter = true, allowMonthFilter, - allowCompare, allowDownload = false, + allowCompare = false, }: { websiteId: string; allowFilter?: boolean; - allowCompare?: boolean; allowDateFilter?: boolean; allowMonthFilter?: boolean; allowDownload?: boolean; + allowCompare?: boolean; }) { return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index e2a6bc36..7637a452 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -13,6 +13,7 @@ import { Network, ChartPie, UserPlus, + Compare, } from '@/components/icons'; import { useMessages, useNavigation } from '@/components/hooks'; import { SideMenu } from '@/components/common/SideMenu'; @@ -22,7 +23,8 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { pathname, renderUrl, teamId } = useNavigation(); - const renderPath = (path: string) => renderUrl(`/websites/${websiteId}${path}`); + const renderPath = (path: string) => + renderUrl(`/websites/${websiteId}${path}`, { event: undefined }); const items = [ { @@ -52,6 +54,12 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { icon: , path: renderPath('/realtime'), }, + { + id: 'compare', + label: formatMessage(labels.compare), + icon: , + path: renderPath('/compare'), + }, { id: 'breakdown', label: formatMessage(labels.breakdown), @@ -132,8 +140,8 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { ]; const selectedKey = - items.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path))?.id || - 'overview'; + items.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path.split('?')[0])) + ?.id || 'overview'; return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx index f32d3a13..9b08427d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx @@ -27,7 +27,7 @@ export function WebsitePage({ websiteId }: { websiteId: string }) { return ( - + diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx new file mode 100644 index 00000000..4c5b7b93 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx @@ -0,0 +1,20 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { CompareTables } from './CompareTables'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar'; +import { Panel } from '@/components/common/Panel'; +import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; + +export function ComparePage({ websiteId }: { websiteId: string }) { + return ( + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx similarity index 58% rename from src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx rename to src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx index 5ca8e9d4..7ba728b5 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx @@ -1,26 +1,27 @@ -import { Grid, Heading, Column, Row, NavMenu, NavMenuItem, Text } from '@umami/react-zen'; +import { useState } from 'react'; +import { Grid, Heading, Column, Row, Select, ListItem } from '@umami/react-zen'; import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { MetricsTable } from '@/components/metrics/MetricsTable'; -import { getCompareDate } from '@/lib/date'; -import { formatNumber } from '@/lib/format'; -import { useState } from 'react'; import { Panel } from '@/components/common/Panel'; import { DateDisplay } from '@/components/common/DateDisplay'; import { ChangeLabel } from '@/components/metrics/ChangeLabel'; +import { getCompareDate } from '@/lib/date'; +import { formatNumber } from '@/lib/format'; -export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { - const [data] = useState([]); +export function CompareTables({ websiteId }: { websiteId: string }) { + const [data, setData] = useState([]); const { dateRange, dateCompare } = useDateRange(websiteId); const { formatMessage, labels } = useMessages(); const { + router, updateParams, - query: { view }, + query: { view = 'path' }, } = useNavigation(); const items = [ { id: 'path', - label: formatMessage(labels.pages), + label: formatMessage(labels.path), path: updateParams({ view: 'path' }), }, { @@ -73,11 +74,6 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { label: formatMessage(labels.events), path: updateParams({ view: 'event' }), }, - { - id: 'query', - label: formatMessage(labels.queryParameters), - path: updateParams({ view: 'query' }), - }, { id: 'hostname', label: formatMessage(labels.hostname), @@ -90,7 +86,8 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { }, ]; - const renderChange = ({ label: x, count: y }) => { + const renderChange = props => { + const { label: x, count: y } = props; const prev = data.find(d => d.x === x)?.y; const value = y - prev; const change = Math.abs(((y - prev) / prev) * 100); @@ -104,6 +101,10 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { ); }; + const handleChange = id => { + router.push(updateParams({ view: id })); + }; + const { startDate, endDate } = getCompareDate( dateCompare, dateRange.startDate, @@ -116,44 +117,53 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { }; return ( - - - - {items.map(({ id, label }) => { - return ( - - {label} - - ); - })} - - - - {formatMessage(labels.previous)} - - - - - - - {formatMessage(labels.current)} - - - - - - + <> + + + + + + + + {formatMessage(labels.previous)} + + + + + + + {formatMessage(labels.current)} + + + + + + + ); } diff --git a/src/app/(main)/websites/[websiteId]/compare/page.tsx b/src/app/(main)/websites/[websiteId]/compare/page.tsx new file mode 100644 index 00000000..2b2cf5b1 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/compare/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { ComparePage } from './ComparePage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Compare', +}; diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 14b89300..48e8b015 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -43,7 +43,7 @@ export function EventsPage({ websiteId }) { websiteId={websiteId} type="event" title={formatMessage(labels.events)} - metric={formatMessage(labels.actions)} + metric={formatMessage(labels.count)} /> diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx index c93b1543..7791ef82 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx @@ -11,8 +11,9 @@ import { import { subMonths, endOfDay } from 'date-fns'; import { FieldFilters } from '@/components/input/FieldFilters'; import { useState } from 'react'; -import { useApi, useMessages, useModified, useWebsiteSegmentQuery } from '@/components/hooks'; +import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks'; import { filtersArrayToObject } from '@/lib/params'; +import { messages } from '@/components/messages'; export function SegmentEditForm({ segmentId, @@ -32,24 +33,23 @@ export function SegmentEditForm({ const { data } = useWebsiteSegmentQuery(websiteId, segmentId); const { formatMessage, labels } = useMessages(); const [currentFilters, setCurrentFilters] = useState(filters); - const { touch } = useModified(); const startDate = subMonths(endOfDay(new Date()), 6); const endDate = endOfDay(new Date()); - const { post, useMutation } = useApi(); - const { mutate, error, isPending } = useMutation({ - mutationFn: (data: any) => - post(`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, { - ...data, - type: 'segment', - }), - }); + const { mutate, error, isPending, touch, toast } = useUpdateQuery( + `/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, + { + ...data, + type: 'segment', + }, + ); const handleSubmit = async (data: any) => { mutate( { ...data, parameters: filtersArrayToObject(currentFilters) }, { onSuccess: async () => { + toast(formatMessage(messages.saved)); touch('segments'); onSave?.(); onClose?.(); diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts index 3b78d94f..605724fc 100644 --- a/src/components/hooks/useFields.ts +++ b/src/components/hooks/useFields.ts @@ -5,10 +5,8 @@ export function useFields() { const fields = [ { name: 'path', type: 'string', label: formatMessage(labels.path) }, - // { name: 'cohort', type: 'string', label: formatMessage(labels.cohort) }, { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) }, { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, - //{ name: 'query', type: 'string', label: formatMessage(labels.query) }, { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, { name: 'os', type: 'string', label: formatMessage(labels.os) }, { name: 'device', type: 'string', label: formatMessage(labels.device) }, @@ -17,6 +15,7 @@ export function useFields() { { name: 'city', type: 'string', label: formatMessage(labels.city) }, { name: 'hostname', type: 'string', label: formatMessage(labels.hostname) }, { name: 'tag', type: 'string', label: formatMessage(labels.tag) }, + { name: 'event', type: 'string', label: formatMessage(labels.event) }, ]; return { fields }; diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index 6f33e687..93050003 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -27,10 +27,11 @@ export function FilterBar({ websiteId }: { websiteId: string }) { router, updateParams, replaceParams, - query: { segment }, + query: { segment, cohort }, } = useNavigation(); const { filters, operatorLabels } = useFilters(); const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment); + const canSave = filters.length > 0 && !segment && !cohort; const handleCloseFilter = (param: string) => { router.push(updateParams({ [param]: undefined })); @@ -78,7 +79,7 @@ export function FilterBar({ websiteId }: { websiteId: string }) { - {!!filters.length && ( + {canSave && ( - {formatMessage(compare ? labels.cancel : labels.compareDates)} - - )} ); } diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index f65b6321..6854f44e 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Icon, Row, Text } from '@umami/react-zen'; import { LinkButton } from '@/components/common/LinkButton'; import { LoadingPanel } from '@/components/common/LoadingPanel'; @@ -15,6 +15,7 @@ export interface MetricsTableProps extends ListTableProps { limit?: number; showMore?: boolean; params?: Record; + onDataLoad?: (data: any) => void; } export function MetricsTable({ @@ -24,6 +25,7 @@ export function MetricsTable({ limit, showMore = false, params, + onDataLoad, ...props }: MetricsTableProps) { const { updateParams } = useNavigation(); @@ -55,6 +57,12 @@ export function MetricsTable({ return []; }, [data, dataFilter, limit, type]); + useEffect(() => { + if (data) { + onDataLoad?.(data); + } + }, [data]); + const renderLabel = (row: any) => { return ; }; diff --git a/src/lib/request.ts b/src/lib/request.ts index c5e5b9bc..3dcf1f6c 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -116,5 +116,6 @@ export async function getQueryFilters( orderBy: params?.orderBy, sortDescending: params?.sortDescending, search: params?.search, + compare: params?.compare, }; }