diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index b2ea2a83..896c733a 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -21,30 +21,28 @@ export function WebsiteChart({ const { pageviews, sessions, compare } = (data || {}) as any; const chartData = useMemo(() => { - if (data) { - const result = { - pageviews, - sessions, - }; + if (!data) { + return { pageviews: [], sessions: [] }; + } - if (compare) { - result.compare = { - pageviews: result.pageviews.map(({ x }, i) => ({ + return { + pageviews, + sessions, + ...(compare && { + compare: { + pageviews: pageviews.map(({ x }, i) => ({ x, y: compare.pageviews[i]?.y, d: compare.pageviews[i]?.x, })), - sessions: result.sessions.map(({ x }, i) => ({ + sessions: sessions.map(({ x }, i) => ({ x, y: compare.sessions[i]?.y, d: compare.sessions[i]?.x, })), - }; - } - - return result; - } - return { pageviews: [], sessions: [] }; + }, + }), + }; }, [data, startDate, endDate, unit]); return ( diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 6c91ba6d..605ee385 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format'; export function WebsiteMetricsBar({ websiteId, + compareMode, }: { websiteId: string; showChange?: boolean; compareMode?: boolean; }) { - const { isAllTime } = useDateRange(); + const { isAllTime, dateCompare } = useDateRange(); const { formatMessage, labels, getErrorMessage } = useMessages(); - const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({ + websiteId, + compare: compareMode ? dateCompare?.compare : undefined, + }); const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {}; diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx index f587e112..5acc9e68 100644 --- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx @@ -1,7 +1,8 @@ 'use client'; -import { Column } from '@umami/react-zen'; +import { Column, Row } from '@umami/react-zen'; import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal'; import { Panel } from '@/components/common/Panel'; +import { UnitFilter } from '@/components/input/UnitFilter'; import { WebsiteChart } from './WebsiteChart'; import { WebsiteControls } from './WebsiteControls'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; @@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) { + + + diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx index bca8d244..32d641b0 100644 --- a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx @@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) { return ( - + diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index b7177b5d..9d21f4f5 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -31,9 +31,11 @@ export async function GET( const data = await getWebsiteStats(websiteId, filters); - const compare = filters.compare ?? 'prev'; - - const { startDate, endDate } = getCompareDate(compare, filters.startDate, filters.endDate); + const { startDate, endDate } = getCompareDate( + filters.compare ?? 'prev', + filters.startDate, + filters.endDate, + ); const comparison = await getWebsiteStats(websiteId, { ...filters, diff --git a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts index b2e90199..1611c7f8 100644 --- a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts +++ b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts @@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); + const { startAt, endAt } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ @@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery( websiteId, startAt, endAt, - unit, - timezone, ...filters, ...params, }, @@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery( get(`/websites/${websiteId}/metrics/expanded`, { startAt, endAt, - unit, - timezone, ...filters, ...params, }), diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts index 67c5e4d4..cd064af6 100644 --- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts +++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts @@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); + const { startAt, endAt } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ @@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery( websiteId, startAt, endAt, - unit, - timezone, ...filters, ...params, }, @@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery( get(`/websites/${websiteId}/metrics`, { startAt, endAt, - unit, - timezone, ...filters, ...params, }), diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts index 69bae09f..48484a07 100644 --- a/src/components/hooks/queries/useWebsiteStatsQuery.ts +++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts @@ -1,6 +1,5 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useDateParameters } from '@/components/hooks/useDateParameters'; -import { useDateRange } from '@/components/hooks/useDateRange'; import { useApi } from '../useApi'; import { useFilterParameters } from '../useFilterParameters'; @@ -20,21 +19,16 @@ export interface WebsiteStatsData { } export function useWebsiteStatsQuery( - websiteId: string, + { websiteId, compare }: { websiteId: string; compare?: string }, options?: UseQueryOptions, ) { const { get, useQuery } = useApi(); - const { startAt, endAt, unit, timezone } = useDateParameters(); - const { compare } = useDateRange(); + const { startAt, endAt } = useDateParameters(); const filters = useFilterParameters(); return useQuery({ - queryKey: [ - 'websites:stats', - { websiteId, startAt, endAt, unit, timezone, compare, ...filters }, - ], - queryFn: () => - get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, compare, ...filters }), + queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }], + queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useWeeklyTrafficQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts index a76ebb3d..df729ffd 100644 --- a/src/components/hooks/queries/useWeeklyTrafficQuery.ts +++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts @@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record { return get(`/websites/${websiteId}/sessions/weekly`, { startAt, endAt, - unit, timezone, ...params, ...filters, diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 755f36ee..5090bd3d 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage'; export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) { const { - query: { date = '', offset = 0, compare = 'prev' }, + query: { date = '', unit = '', offset = 0, compare = 'prev' }, } = useNavigation(); const { locale } = useLocale(); - const dateRange = useMemo(() => { const dateRangeObject = parseDateRange( date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE, + unit, locale, options.timezone, ); @@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin return !options.ignoreOffset && offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject; - }, [date, offset, options]); + }, [date, unit, offset, options]); const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate); return { date, + unit, offset, compare, isAllTime: date.endsWith(`:all`), diff --git a/src/components/input/UnitFilter.tsx b/src/components/input/UnitFilter.tsx new file mode 100644 index 00000000..84a15f35 --- /dev/null +++ b/src/components/input/UnitFilter.tsx @@ -0,0 +1,71 @@ +import { ListItem, Row, Select } from '@umami/react-zen'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants'; +import { getItem } from '@/lib/storage'; + +export function UnitFilter() { + const { formatMessage, labels } = useMessages(); + const { router, query, updateParams } = useNavigation(); + + const DATE_RANGE_UNIT_CONFIG = { + '0week': { + defaultUnit: 'day', + availableUnits: ['day', 'hour'], + }, + '7day': { + defaultUnit: 'day', + availableUnits: ['day', 'hour'], + }, + '0month': { + defaultUnit: 'day', + availableUnits: ['day', 'hour'], + }, + '30day': { + defaultUnit: 'day', + availableUnits: ['day', 'hour'], + }, + '90day': { + defaultUnit: 'day', + availableUnits: ['day', 'month'], + }, + '6month': { + defaultUnit: 'month', + availableUnits: ['month', 'day'], + }, + }; + + const unitConfig = + DATE_RANGE_UNIT_CONFIG[query.date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE]; + + if (!unitConfig) { + return null; + } + + const handleChange = (value: string) => { + router.push(updateParams({ unit: value })); + }; + + const options = unitConfig.availableUnits.map(unit => ({ + id: unit, + label: formatMessage(labels[unit]), + })); + + const selectedUnit = query.unit ?? unitConfig.defaultUnit; + + return ( + + + + ); +} diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 18b4f13b..a76058ec 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -41,7 +41,7 @@ export function WebsiteDateFilter({ }), ); } else { - router.push(updateParams({ date, offset: undefined })); + router.push(updateParams({ date, offset: undefined, unit: undefined })); } }; diff --git a/src/components/messages.ts b/src/components/messages.ts index 712495d8..3d7388cd 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -245,7 +245,10 @@ export const labels = defineMessages({ tag: { id: 'label.tag', defaultMessage: 'Tag' }, segment: { id: 'label.segment', defaultMessage: 'Segment' }, cohort: { id: 'label.cohort', defaultMessage: 'Cohort' }, + minute: { id: 'label.minute', defaultMessage: 'Minute' }, + hour: { id: 'label.hour', defaultMessage: 'Hour' }, day: { id: 'label.day', defaultMessage: 'Day' }, + month: { id: 'label.month', defaultMessage: 'Month' }, date: { id: 'label.date', defaultMessage: 'Date' }, pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' }, create: { id: 'label.create', defaultMessage: 'Create' }, diff --git a/src/lib/date.ts b/src/lib/date.ts index 3c1fd1b7..a005bf8e 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -9,6 +9,7 @@ import { differenceInCalendarMonths, differenceInCalendarWeeks, differenceInCalendarYears, + differenceInDays, differenceInHours, differenceInMinutes, endOfDay, @@ -136,7 +137,12 @@ export function parseDateValue(value: string) { return { num: +num, unit }; } -export function parseDateRange(value: string, locale = 'en-US', timezone?: string): DateRange { +export function parseDateRange( + value: string, + unitValue?: string, + locale = 'en-US', + timezone?: string, +): DateRange { if (typeof value !== 'string') { return null; } @@ -146,7 +152,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin const startDate = new Date(+startTime); const endDate = new Date(+endTime); - const unit = getMinimumUnit(startDate, endDate); + const unit = getMinimumUnit(startDate, endDate, true); return { startDate, @@ -169,14 +175,14 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin endDate: endOfHour(now), offset: 0, num: num || 1, - unit, + unit: unitValue || unit, value, }; case 'day': return { startDate: num ? subDays(startOfDay(now), num) : startOfDay(now), endDate: endOfDay(now), - unit: num ? 'day' : 'hour', + unit: unitValue ? unitValue : num ? 'day' : 'hour', offset: 0, num: num || 1, value, @@ -187,7 +193,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin ? subWeeks(startOfWeek(now, { locale: dateLocale }), num) : startOfWeek(now, { locale: dateLocale }), endDate: endOfWeek(now, { locale: dateLocale }), - unit: 'day', + unit: unitValue || 'day', offset: 0, num: num || 1, value, @@ -196,7 +202,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin return { startDate: num ? subMonths(startOfMonth(now), num) : startOfMonth(now), endDate: endOfMonth(now), - unit: num ? 'month' : 'day', + unit: unitValue ? unitValue : num ? 'month' : 'day', offset: 0, num: num || 1, value, @@ -205,7 +211,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin return { startDate: num ? subYears(startOfYear(now), num) : startOfYear(now), endDate: endOfYear(now), - unit: 'month', + unit: unitValue || 'month', offset: 0, num: num || 1, value, @@ -273,10 +279,18 @@ export function getAllowedUnits(startDate: Date, endDate: Date) { return index >= 0 ? units.splice(index) : []; } -export function getMinimumUnit(startDate: number | Date, endDate: number | Date) { +export function getMinimumUnit( + startDate: number | Date, + endDate: number | Date, + isDateRange: boolean = false, +) { if (differenceInMinutes(endDate, startDate) <= 60) { return 'minute'; - } else if (differenceInHours(endDate, startDate) <= 48) { + } else if ( + isDateRange + ? differenceInHours(endDate, startDate) <= 48 + : differenceInDays(endDate, startDate) <= 30 + ) { return 'hour'; } else if (differenceInCalendarMonths(endDate, startDate) <= 6) { return 'day';