From ee8750d9df65843006f8e73e88adee12ddaa704b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 28 Jun 2025 21:16:50 -0700 Subject: [PATCH] Update Retention report. --- .../[websiteId]/WebsiteChart.module.css | 17 ---- .../WebsiteCompareTables.module.css | 14 --- .../websites/[websiteId]/WebsiteControls.tsx | 10 +- .../websites/[websiteId]/WebsiteHeader.tsx | 18 ++-- .../[websiteId]/WebsiteMetricsBar.module.css | 52 ---------- .../[websiteId]/events/EventsMetricsBar.tsx | 46 ++++----- .../[websiteId]/realtime/RealtimeLog.tsx | 10 +- .../[websiteId]/reports/ReportsNav.tsx | 8 +- .../reports/retention/Retention.tsx | 96 ++++++++++--------- .../reports/retention/RetentionPage.tsx | 10 +- .../sessions/SessionsMetricsBar.tsx | 46 ++++----- src/app/api/reports/retention/route.ts | 2 + src/components/common/Panel.tsx | 2 +- src/components/hooks/useDateRange.ts | 2 +- src/components/hooks/useFilterParams.ts | 4 +- src/components/input/DateFilter.tsx | 2 +- src/components/input/MonthSelect.tsx | 91 +++++++----------- src/components/input/WebsiteDateFilter.tsx | 2 +- src/components/input/WebsiteMonthSelect.tsx | 17 ++++ src/components/messages.ts | 4 +- src/lib/date.ts | 4 + src/queries/sql/reports/getRetention.ts | 37 +++---- 22 files changed, 214 insertions(+), 280 deletions(-) delete mode 100644 src/app/(main)/websites/[websiteId]/WebsiteChart.module.css delete mode 100644 src/app/(main)/websites/[websiteId]/WebsiteCompareTables.module.css delete mode 100644 src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css create mode 100644 src/components/input/WebsiteMonthSelect.tsx diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css b/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css deleted file mode 100644 index b795047a..00000000 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.container { - position: relative; - display: flex; - flex-direction: column; - align-self: stretch; -} - -.chart { - position: relative; - overflow: hidden; -} - -.title { - font-size: var(--font-size-lg); - line-height: 60px; - font-weight: 600; -} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.module.css b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.module.css deleted file mode 100644 index c4821e88..00000000 --- a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.container { - margin-bottom: 60px; -} - -.nav { - width: 200px; - margin-top: 40px; -} - -.title { - color: var(--base800); - text-align: center; - font-weight: 700; -} diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index fb84df87..1e36b729 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -2,23 +2,27 @@ import { Column, Row } from '@umami/react-zen'; import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { FilterBar } from '@/components/input/FilterBar'; +import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; export function WebsiteControls({ websiteId, allowFilter = true, + allowDateFilter = true, + allowMonthFilter, allowCompare, }: { websiteId: string; allowFilter?: boolean; allowCompare?: boolean; + allowDateFilter?: boolean; + allowMonthFilter?: boolean; }) { return ( {allowFilter && } - - - + {allowDateFilter && } + {allowMonthFilter && } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index d0a17b7f..4c247dcd 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -14,15 +14,17 @@ export function WebsiteHeader() { return ( } showBorder={false}> - + - - - - - - Edit - + + + + + + + Edit + + ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css deleted file mode 100644 index 6c5a0e56..00000000 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css +++ /dev/null @@ -1,52 +0,0 @@ -.container { - display: grid; - grid-template-columns: 2fr 1fr; - justify-content: space-between; - align-items: center; - background: var(--base50); - z-index: var(--z-index-above); - min-height: 120px; - padding-bottom: 20px; -} - -.actions { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 10px; - flex-wrap: wrap; -} - -.vs { - display: flex; - align-items: center; - justify-content: flex-end; - flex-basis: 100%; - gap: 10px; -} - -.dropdown { - min-width: 200px; -} - -@media screen and (max-width: 1200px) { - .container { - grid-template-columns: 1fr; - } - - .actions { - margin: 20px 0; - } -} - -@media screen and (min-width: 992px) { - .sticky { - position: sticky; - top: -1px; - } - - .isSticky { - padding: 10px 0; - border-bottom: 1px solid var(--base300); - } -} diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx index 5200ab16..2712c985 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx @@ -11,28 +11,30 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) { return ( - - - - - - + {data && ( + + + + + + + )} ); } diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index c46ef097..8e965528 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -67,7 +67,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { country: string; device: string; }) => { - const { __type, eventName, urlPath: url, browser, os, country, device } = log; + const { __type, eventName, urlPath, browser, os, country, device } = log; if (__type === TYPE_EVENT) { return formatMessage(messages.eventLog, { @@ -75,12 +75,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { url: ( - {url} + {urlPath} ), }); @@ -89,12 +89,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { if (__type === TYPE_PAGEVIEW) { return ( - {url} + {urlPath} ); } diff --git a/src/app/(main)/websites/[websiteId]/reports/ReportsNav.tsx b/src/app/(main)/websites/[websiteId]/reports/ReportsNav.tsx index 6313ca46..f073a12d 100644 --- a/src/app/(main)/websites/[websiteId]/reports/ReportsNav.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/ReportsNav.tsx @@ -66,7 +66,13 @@ export function ReportsNav({ websiteId }: { websiteId: string }) { const isSelected = selected === id; return ( - + {icon} diff --git a/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx index 1667c483..b4bd01a7 100644 --- a/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Grid, Row, Column, Text, Icon } from '@umami/react-zen'; import { Users } from '@/components/icons'; -import { useMessages, useLocale, useResultQuery } from '@/components/hooks'; +import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks'; import { formatDate } from '@/lib/date'; import { formatLongNumber } from '@/lib/format'; import { Panel } from '@/components/common/Panel'; @@ -19,8 +19,10 @@ export interface RetentionProps { export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) { const { formatMessage, labels } = useMessages(); const { locale } = useLocale(); + const { timezone } = useTimezone(); const { data, error, isLoading } = useResultQuery('retention', { websiteId, + timezone, dateRange: { startDate, endDate, @@ -51,54 +53,56 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent return ( - - - - - - {formatMessage(labels.cohort)} - - - {days.map(n => ( - - - {formatMessage(labels.day)} {n} + {data && ( + + + + + + {formatMessage(labels.cohort)} - ))} - - {rows.map(({ date, visitors, records }: any, rowIndex: number) => { - return ( - - - {formatDate(date, 'PP', locale)} - - - - - {formatLongNumber(visitors)} - + {days.map(n => ( + + + {formatMessage(labels.day)} {n} + - {days.map(day => { - if (totalDays - rowIndex < day) { - return null; - } - const percentage = records.filter(a => a.day === day)[0]?.percentage; - return ( - {percentage ? `${Number(percentage).toFixed(2)}%` : ''} - ); - })} - - ); - })} - - + ))} + + {rows.map(({ date, visitors, records }: any, rowIndex: number) => { + return ( + + + {formatDate(date, 'PP', locale)} + + + + + {formatLongNumber(visitors)} + + + {days.map(day => { + if (totalDays - rowIndex < day) { + return null; + } + const percentage = records.filter(a => a.day === day)[0]?.percentage; + return ( + {percentage ? `${Number(percentage).toFixed(2)}%` : ''} + ); + })} + + ); + })} + + + )} ); } diff --git a/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx b/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx index 71ffc6fb..64cf6823 100644 --- a/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx @@ -3,16 +3,20 @@ import { Column } from '@umami/react-zen'; import { Retention } from './Retention'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { useDateRange } from '@/components/hooks'; +import { endOfMonth, startOfMonth } from 'date-fns'; export function RetentionPage({ websiteId }: { websiteId: string }) { const { - dateRange: { startDate, endDate }, + dateRange: { startDate }, } = useDateRange(websiteId); + const monthStartDate = startOfMonth(startDate); + const monthEndDate = endOfMonth(startDate); + return ( - - + + ); } diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx index 1da45000..b959f0da 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx @@ -11,28 +11,30 @@ export function SessionsMetricsBar({ websiteId }: { websiteId: string }) { return ( - - - - - - + {data && ( + + + + + + + )} ); } diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index 04842121..cc7433aa 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -14,6 +14,7 @@ export async function POST(request: Request) { const { websiteId, dateRange: { startDate, endDate }, + timezone, } = body; if (!(await canViewWebsite(auth, websiteId))) { @@ -23,6 +24,7 @@ export async function POST(request: Request) { const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), + timezone, }); return json(data); diff --git a/src/components/common/Panel.tsx b/src/components/common/Panel.tsx index 22f9ab9b..7341b3b5 100644 --- a/src/components/common/Panel.tsx +++ b/src/components/common/Panel.tsx @@ -62,7 +62,7 @@ export function Panel({ - {formatMessage(labels.expand)} + {formatMessage(labels.maximize)} )} diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 6a6e16d4..795c4239 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -22,7 +22,7 @@ export function useDateRange(websiteId?: string) { ); const dateRange = useMemo( () => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject), - [date, offset], + [date, offset, websiteConfig], ); const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE); diff --git a/src/components/hooks/useFilterParams.ts b/src/components/hooks/useFilterParams.ts index 55deed14..bee2a649 100644 --- a/src/components/hooks/useFilterParams.ts +++ b/src/components/hooks/useFilterParams.ts @@ -8,7 +8,7 @@ export function useFilterParams(websiteId: string) { const { timezone, toUtc } = useTimezone(); const { query: { - url, + path, referrer, title, query, @@ -29,7 +29,7 @@ export function useFilterParams(websiteId: string) { endAt: +toUtc(endDate), unit, timezone, - url, + path, referrer, title, query, diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index 72be6c6d..3e2c00ed 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -99,7 +99,7 @@ export function DateFilter({ }; return ( - + + {months.map(m => { + return ( + + {formatDate(new Date(year, m, 1), 'MMMM', locale)} + + ); + })} + + ); } diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index afb58631..e78bcd8d 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -55,7 +55,7 @@ export function WebsiteDateFilter({ }; return ( - + {showButtons && !isAllTime && !isCustomRange && (