Compare commits

...

3 commits

Author SHA1 Message Date
Francis Cao
355fa1e9a9 increase minimum unit for day. Last 6 months includes current month
Some checks are pending
Node.js CI / build (push) Waiting to run
2026-01-23 11:42:12 -08:00
Francis Cao
a2f0066c42 clear unit param on nav, fix more import, fix NaN animation error 2026-01-23 11:19:19 -08:00
Francis Cao
e73432dd26 Add unit select and backend implementation. Fix compare for websitestats. Remove unused params from stats, metrics, weekly 2026-01-23 10:08:47 -08:00
17 changed files with 144 additions and 61 deletions

View file

@ -21,30 +21,28 @@ export function WebsiteChart({
const { pageviews, sessions, compare } = (data || {}) as any;
const chartData = useMemo(() => {
if (data) {
const result = {
if (!data) {
return { pageviews: [], sessions: [] };
}
return {
pageviews,
sessions,
};
if (compare) {
result.compare = {
pageviews: result.pageviews.map(({ x }, i) => ({
...(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 (

View file

@ -10,7 +10,7 @@ import {
} from '@umami/react-zen';
import { Fragment } from 'react';
import { useMessages, useNavigation } from '@/components/hooks';
import { Edit, More, Share } from '@/components/icons';
import { Edit, MoreHorizontal, Share } from '@/components/icons';
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
@ -33,7 +33,7 @@ export function WebsiteMenu({ websiteId }: { websiteId: string }) {
<MenuTrigger>
<Button variant="quiet">
<Icon>
<More />
<MoreHorizontal />
</Icon>
</Button>
<Popover placement="bottom">

View file

@ -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 || {};

View file

@ -29,6 +29,7 @@ export function WebsiteNav({
event: undefined,
compare: undefined,
view: undefined,
unit: undefined,
});
const items = [

View file

@ -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 }) {
<WebsiteControls websiteId={websiteId} />
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
<Panel minHeight="520px">
<Row justifyContent="end">
<UnitFilter />
</Row>
<WebsiteChart websiteId={websiteId} />
</Panel>
<WebsitePanels websiteId={websiteId} />

View file

@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
return (
<Column gap>
<WebsiteControls websiteId={websiteId} allowCompare={true} />
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showChange={true} />
<Panel minHeight="520px">
<WebsiteChart websiteId={websiteId} compareMode={true} />
</Panel>

View file

@ -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,

View file

@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
) {
const { get, useQuery } = useApi();
const { startAt, endAt, unit, timezone } = useDateParameters();
const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery<WebsiteExpandedMetricsData>({
@ -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,
}),

View file

@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
options?: ReactQueryOptions<WebsiteMetricsData>,
) {
const { get, useQuery } = useApi();
const { startAt, endAt, unit, timezone } = useDateParameters();
const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery<WebsiteMetricsData>({
@ -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,
}),

View file

@ -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<WebsiteStatsData, Error, WebsiteStatsData>,
) {
const { get, useQuery } = useApi();
const { startAt, endAt, unit, timezone } = useDateParameters();
const { compare } = useDateRange();
const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery<WebsiteStatsData>({
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,
});

View file

@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string,
return useQuery({
queryKey: [
'sessions',
{ websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
{ websiteId, modified, startAt, endAt, timezone, ...params, ...filters },
],
queryFn: () => {
return get(`/websites/${websiteId}/sessions/weekly`, {
startAt,
endAt,
unit,
timezone,
...params,
...filters,

View file

@ -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`),

View file

@ -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 (
<Row>
<Select
value={selectedUnit}
onChange={handleChange}
popoverProps={{ placement: 'bottom right' }}
style={{ width: 100 }}
>
{options.map(({ id, label }) => (
<ListItem key={id} id={id}>
{label}
</ListItem>
))}
</Select>
</Row>
);
}

View file

@ -41,7 +41,7 @@ export function WebsiteDateFilter({
}),
);
} else {
router.push(updateParams({ date, offset: undefined }));
router.push(updateParams({ date, offset: undefined, unit: undefined }));
}
};

View file

@ -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' },

View file

@ -25,7 +25,7 @@ export const MetricCard = ({
showChange = false,
}: MetricCardProps) => {
const diff = value - change;
const pct = ((value - diff) / diff) * 100;
const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });

View file

@ -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,12 +279,20 @@ 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) {
} else if (differenceInCalendarMonths(endDate, startDate) <= 7) {
return 'day';
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
return 'month';