"use client"; import { Calendar as CalendarIcon } from "@openstatus/icons"; import type { DatePreset } from "@openstatus/ui/components/data-table-filters/types"; import { Button } from "@openstatus/ui/components/ui/button"; import { Calendar } from "@openstatus/ui/components/ui/calendar"; import { Input } from "@openstatus/ui/components/ui/input"; import { Kbd } from "@openstatus/ui/components/ui/kbd"; import { Label } from "@openstatus/ui/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@openstatus/ui/components/ui/popover"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@openstatus/ui/components/ui/select"; import { Separator } from "@openstatus/ui/components/ui/separator"; import { useDebounce } from "@openstatus/ui/hooks/use-debounce"; import { presets as defaultPresets } from "@openstatus/ui/lib/date-preset"; import { cn } from "@openstatus/ui/lib/utils"; import { endOfDay, format } from "date-fns"; import * as React from "react"; import type { DateRange } from "react-day-picker"; interface DatePickerWithRangeProps extends React.HTMLAttributes { date: DateRange | undefined; setDate: (date: DateRange | undefined) => void; presets?: DatePreset[]; } /** The presets span the selectable window - nothing outside them is queryable. */ function getBounds(presets: DatePreset[]) { const from = presets.map((preset) => preset.from.getTime()); const to = presets.map((preset) => preset.to.getTime()); return { min: from.length ? new Date(Math.min(...from)) : undefined, max: to.length ? new Date(Math.max(...to)) : undefined, }; } function clampToBounds(date: Date | undefined, min?: Date, max?: Date) { if (!date) return undefined; if (min && date < min) return min; if (max && date > max) return max; return date; } export function DatePickerWithRange({ className, date, setDate, presets = defaultPresets, }: DatePickerWithRangeProps) { const [open, setOpen] = React.useState(false); const { min, max } = React.useMemo(() => getBounds(presets), [presets]); const onCalendarSelect = React.useCallback( (range: DateRange | undefined) => { if (!range) { setDate(undefined); return; } // A day in the calendar means that whole day, but react-day-picker // returns its midnight - without this the last day gets excluded. setDate({ from: clampToBounds(range.from, min, max), to: clampToBounds(range.to ? endOfDay(range.to) : undefined, min, max), }); }, [setDate, min, max], ); React.useEffect(() => { const down = (e: KeyboardEvent) => { if (!open) return; presets.map((preset) => { if (preset.shortcut === e.key) { setDate({ from: preset.from, to: preset.to }); } }); }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, [setDate, presets, open]); return (
); } function DatePresets({ selected, onSelect, presets, }: { selected: DateRange | undefined; onSelect: (date: DateRange | undefined) => void; presets: DatePreset[]; }) { return (

Date Range

{presets.map(({ label, shortcut, from, to }) => { const isActive = selected?.from === from && selected?.to === to; return ( ); })}
); } function DatePresetsSelect({ selected, onSelect, presets, }: { selected: DateRange | undefined; onSelect: (date: DateRange | undefined) => void; presets: DatePreset[]; }) { function findPreset(from?: Date, to?: Date) { return presets.find((p) => p.from === from && p.to === to)?.shortcut; } const [value, setValue] = React.useState( findPreset(selected?.from, selected?.to), ); React.useEffect(() => { const preset = findPreset(selected?.from, selected?.to); if (preset === value) return; setValue(preset); }, [selected, presets]); return ( ); } function CustomDateRange({ selected, onSelect, min, max, }: { selected: DateRange | undefined; onSelect: (date: DateRange | undefined) => void; min?: Date; max?: Date; }) { const [range, setRange] = React.useState(selected); const debouncedRange = useDebounce(range, 1000); const selectedFrom = selected?.from?.getTime(); const selectedTo = selected?.to?.getTime(); const formatDateForInput = (date: Date | undefined): string => { if (!date) return ""; const utcDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); return utcDate.toISOString().slice(0, 16); }; /** Presets and the calendar write `selected` - adopt it as the new draft, * otherwise editing one endpoint submits the other one from a stale range. */ React.useEffect(() => { setRange(selected); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFrom, selectedTo]); React.useEffect(() => { if ( debouncedRange?.from?.getTime() === selectedFrom && debouncedRange?.to?.getTime() === selectedTo ) { return; } onSelect(debouncedRange); // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedRange]); /** The calendar disables anything outside the presets - typing must not * slip past the same window. */ const isWithinBounds = (date: Date) => !(min && date < min) && !(max && date > max); return (

Custom Range

{ const newDate = new Date(e.target.value); if (Number.isNaN(newDate.getTime())) return; if (!isWithinBounds(newDate)) return; setRange((prev) => ({ from: newDate, to: prev?.to })); }} disabled={!selected?.from} />
{ const newDate = new Date(e.target.value); if (Number.isNaN(newDate.getTime())) return; if (!isWithinBounds(newDate)) return; setRange((prev) => ({ from: prev?.from, to: newDate })); }} disabled={!selected?.to} />
); }