feat: Add time-of-day filtering with quick hour filters and time range picker

This commit is contained in:
Yash 2026-01-23 20:40:37 +05:30
parent 860e6390f1
commit c5cb032e05
3 changed files with 152 additions and 0 deletions

View file

@ -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 ? (
<DateDisplay startDate={startDate} endDate={endDate} />
@ -136,6 +172,20 @@ export function DateFilter({
</Dialog>
</Modal>
)}
{showTimePicker && (
<Modal isOpen={true}>
<Dialog>
<TimeRangePickerForm
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
maxDate={endOfYear(new Date())}
onChange={handleTimePickerChange}
onClose={() => setShowTimePicker(false)}
/>
</Dialog>
</Modal>
)}
</>
);
}

View file

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

View file

@ -0,0 +1,99 @@
import { Button, Calendar, Column, ListItem, Row, Select } from '@umami/react-zen';
import { endOfDay, isAfter, 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(0);
const [endHour, setEndHour] = useState(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), 59);
onChange(`range:${start.getTime()}:${end.getTime()}`);
};
const handleStartHourChange = (value: Key) => {
setStartHour(Number(value));
};
const handleEndHourChange = (value: Key) => {
setEndHour(Number(value));
};
return (
<Column gap>
<Row justifyContent="center">
<Calendar value={date} minValue={minDate} maxValue={maxDate} onChange={setDate} />
</Row>
<Row gap justifyContent="center" alignItems="center">
<Column>
<label style={{ fontSize: '0.875rem', marginBottom: '0.25rem' }}>
{formatMessage(labels.startTime)}
</label>
<Select
value={startHour.toString()}
onChange={handleStartHourChange}
style={{ minWidth: '100px' }}
>
{HOUR_OPTIONS.map(({ id, label }) => (
<ListItem key={id} id={id}>
{label}
</ListItem>
))}
</Select>
</Column>
<span style={{ marginTop: '1.5rem' }}></span>
<Column>
<label style={{ fontSize: '0.875rem', marginBottom: '0.25rem' }}>
{formatMessage(labels.endTime)}
</label>
<Select
value={endHour.toString()}
onChange={handleEndHourChange}
style={{ minWidth: '100px' }}
>
{HOUR_OPTIONS.map(({ id, label }) => (
<ListItem key={id} id={id}>
{label}
</ListItem>
))}
</Select>
</Column>
</Row>
<Row justifyContent="end" gap>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<Button variant="primary" onPress={handleSave} isDisabled={disabled}>
{formatMessage(labels.apply)}
</Button>
</Row>
</Column>
);
}