diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx
index 2e175298..d5bdc0b5 100644
--- a/src/components/input/DateFilter.tsx
+++ b/src/components/input/DateFilter.tsx
@@ -4,6 +4,7 @@ import { Fragment, type Key, useState } from 'react';
import { DateDisplay } from '@/components/common/DateDisplay';
import { useMessages, useMobile } from '@/components/hooks';
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
+import { TimeRangePickerForm } from '@/components/metrics/TimeRangePickerForm';
import { parseDateRange } from '@/lib/date';
export interface DateFilterProps extends SelectProps {
@@ -24,6 +25,7 @@ export function DateFilter({
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
+ const [showTimePicker, setShowTimePicker] = useState(false);
const { startDate, endDate } = parseDateRange(value) || {};
const { isMobile } = useMobile();
@@ -33,6 +35,27 @@ export function DateFilter({
label: formatMessage(labels.lastHours, { x: '24' }),
value: '24hour',
},
+ {
+ label: formatMessage(labels.lastHours, { x: '12' }),
+ value: '12hour',
+ },
+ {
+ label: formatMessage(labels.lastHours, { x: '6' }),
+ value: '6hour',
+ },
+ {
+ label: formatMessage(labels.lastHours, { x: '4' }),
+ value: '4hour',
+ },
+ {
+ label: formatMessage(labels.lastHours, { x: '2' }),
+ value: '2hour',
+ },
+ {
+ label: formatMessage(labels.lastHours, { x: '1' }),
+ value: '1hour',
+ divider: true,
+ },
{
label: formatMessage(labels.thisWeek),
value: '0week',
@@ -75,6 +98,10 @@ export function DateFilter({
value: 'custom',
divider: true,
},
+ {
+ label: formatMessage(labels.timeRange),
+ value: 'timeRange',
+ },
]
.filter(n => n)
.map((a, id) => ({ ...a, id }));
@@ -84,6 +111,10 @@ export function DateFilter({
setShowPicker(true);
return;
}
+ if (value === 'timeRange') {
+ setShowTimePicker(true);
+ return;
+ }
onChange(value.toString());
};
@@ -92,6 +123,11 @@ export function DateFilter({
onChange(value.toString());
};
+ const handleTimePickerChange = (value: string) => {
+ setShowTimePicker(false);
+ onChange(value.toString());
+ };
+
const renderValue = ({ defaultChildren }) => {
return value?.startsWith('range') || renderDate ? (
@@ -136,6 +172,20 @@ export function DateFilter({
)}
+ {showTimePicker && (
+
+
+
+ )}
>
);
}
diff --git a/src/components/messages.ts b/src/components/messages.ts
index cebb8c1d..8ccc1192 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -133,6 +133,9 @@ export const labels = defineMessages({
thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
+ timeRange: { id: 'label.time-range', defaultMessage: 'Time range' },
+ startTime: { id: 'label.start-time', defaultMessage: 'Start time' },
+ endTime: { id: 'label.end-time', defaultMessage: 'End time' },
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
diff --git a/src/components/metrics/TimeRangePickerForm.tsx b/src/components/metrics/TimeRangePickerForm.tsx
new file mode 100644
index 00000000..5c6cb71b
--- /dev/null
+++ b/src/components/metrics/TimeRangePickerForm.tsx
@@ -0,0 +1,99 @@
+import { Button, Calendar, Column, ListItem, Row, Select } from '@umami/react-zen';
+import { setHours, setMinutes, startOfDay } from 'date-fns';
+import { type Key, useState } from 'react';
+import { useMessages } from '@/components/hooks';
+
+// Generate hour options (00:00 to 23:00)
+const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => ({
+ id: i.toString(),
+ label: `${i.toString().padStart(2, '0')}:00`,
+ value: i,
+}));
+
+interface TimeRangePickerFormProps {
+ startDate?: Date;
+ endDate?: Date;
+ minDate?: Date;
+ maxDate?: Date;
+ onChange: (value: string) => void;
+ onClose: () => void;
+}
+
+export function TimeRangePickerForm({
+ startDate: defaultStartDate,
+ endDate: defaultEndDate,
+ minDate,
+ maxDate,
+ onChange,
+ onClose,
+}: TimeRangePickerFormProps) {
+ const [date, setDate] = useState(defaultStartDate || new Date());
+ const [startHour, setStartHour] = useState(defaultStartDate?.getHours() || 0);
+ const [endHour, setEndHour] = useState(defaultEndDate?.getHours() || 23);
+ const { formatMessage, labels } = useMessages();
+
+ const disabled = startHour > endHour;
+
+ const handleSave = () => {
+ const start = setMinutes(setHours(startOfDay(date), startHour), 0);
+ const end = setMinutes(setHours(startOfDay(date), endHour + 1), 0);
+ onChange(`range:${start.getTime()}:${end.getTime()}`);
+ };
+
+ const handleStartHourChange = (value: Key) => {
+ setStartHour(Number(value));
+ };
+
+ const handleEndHourChange = (value: Key) => {
+ setEndHour(Number(value));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+ );
+}