);
}
export type TimeRangePickerProps = {
timeRange?: TimeRange;
onTimeRangeChange: (timeRange: TimeRange) => void;
timeRangePresets: readonly string[];
className?: string;
disabled?: boolean | { before?: Date; after?: Date } | Date | Date[];
};
export function TimeRangePicker({
className,
timeRange,
timeRangePresets,
onTimeRangeChange,
disabled,
}: TimeRangePickerProps) {
// Determine the range type
const rangeType: "named" | "custom" | null = timeRange
? "from" in timeRange
? "custom"
: "named"
: null;
// Disable future dates by default, plus any additional disabled prop
const calendarDisabled = React.useMemo(() => {
const futureDisabled = { after: new Date() };
if (!disabled) return futureDisabled;
if (typeof disabled === "boolean") return disabled;
// Always return an array when combining with additional restrictions
const disabledArray = Array.isArray(disabled) ? disabled : [disabled];
return [...disabledArray, futureDisabled] as React.ComponentProps<
typeof Calendar
>["disabled"];
}, [disabled]);
const namedRangeValue =
rangeType === "named" && timeRange && "range" in timeRange
? timeRange.range
: null;
// Convert TimeRange to DateRange for internal use
const dateRange = timeRange && "from" in timeRange ? timeRange : undefined;
const [internalDateRange, setInternalDateRange] = useState<
RDPDateRange | undefined
>(dateRange);
// Update internal date range when timeRange changes
useEffect(() => {
if (rangeType === "custom") {
// Custom range - use as is
setInternalDateRange(dateRange);
} else if (rangeType === "named" && timeRange && "range" in timeRange) {
// Preset range - look up in generic time ranges
const setting = TIME_RANGES[timeRange.range as keyof typeof TIME_RANGES];
if (setting && setting.minutes) {
const now = new Date();
setInternalDateRange({
from: addMinutes(now, -setting.minutes),
to: now,
});
} else {
setInternalDateRange(undefined);
}
} else {
setInternalDateRange(undefined);
}
}, [timeRange, dateRange, rangeType]);
const setNewDateRange = (
internalDateRange: RDPDateRange | undefined,
newFromDate: Date | undefined,
newToDate: Date | undefined,
): RDPDateRange | undefined => {
return internalDateRange
? {
from: newFromDate ?? internalDateRange.from,
to: newToDate ?? internalDateRange.to,
}
: undefined;
};
const updateDateRange = (newRange: RDPDateRange | undefined) => {
if (newRange && newRange.from && newRange.to) {
onTimeRangeChange({
from: newRange.from,
to: newRange.to,
});
}
};
const onCalendarSelection = (range?: RDPDateRange) => {
const newRange = range
? {
from: range.from ? setBeginningOfDay(range.from) : undefined,
to: range.to ? setEndOfDay(range.to) : undefined,
}
: undefined;
setInternalDateRange(newRange);
updateDateRange(newRange);
};
const onStartTimeSelection = (date: Date | undefined) => {
const newDateTime = combineDateAndTime(internalDateRange?.from, date);
const newRange = setNewDateRange(
internalDateRange,
newDateTime,
internalDateRange?.to,
);
setInternalDateRange(newRange);
updateDateRange(newRange);
};
const onEndTimeSelection = (date: Date | undefined) => {
const newDateTime = combineDateAndTime(internalDateRange?.to, date);
const newRange = setNewDateRange(
internalDateRange,
internalDateRange?.from,
newDateTime,
);
setInternalDateRange(newRange);
updateDateRange(newRange);
};
const onPresetSelection = (value: string) => {
if (timeRangePresets.includes(value as keyof typeof TIME_RANGES)) {
onTimeRangeChange({ range: value });
}
};
const [isOpen, setIsOpen] = useState(false);
const [tab, setTab] = useState<"presets" | "calendar">("presets");
const handleOpenChange = useCallback((open: boolean) => {
setIsOpen(open);
if (open) {
setTab("presets");
}
}, []);
const getDisplayContent = () => {
if (rangeType === "custom") {
// Custom range - show calendar icon and date range
return (
{dateRange
? formatDateRange(dateRange.from, dateRange.to)
: "Select from calendar"}
);
} else if (rangeType === "named") {
// Preset range - show badge with abbreviation and label
const setting = TIME_RANGES[namedRangeValue as keyof typeof TIME_RANGES];
return (