diff --git a/src/components/filters/DateSelector.tsx b/src/components/filters/DateSelector.tsx index 3d1523ce1..21d9732ff 100644 --- a/src/components/filters/DateSelector.tsx +++ b/src/components/filters/DateSelector.tsx @@ -16,7 +16,6 @@ import { getDefaultClassNames, type PropsSingle, } from "react-day-picker"; -import { useFormatters } from "../../utils/useFormatters"; interface IProps { unsetLabel: string; @@ -30,10 +29,27 @@ interface IProps { referenceDate?: Date; } +// "YYYY-MM-DDTHH:mm" in local time, the format expects. +function toDatetimeLocalString(date: Date): string { + const pad = (n: number) => String(n).padStart(2, "0"); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad( + date.getDate() + )}T${pad(date.getHours())}:${pad(date.getMinutes())}`; +} + +function formatDisplay(date: Date): string { + return date.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + const DateSelector = ({ value, onChange, unsetLabel, minDate, maxDate, referenceDate }: IProps) => { const [isOpen, setIsOpen] = useState(false); const nodeId = useFloatingNodeId(); - const { formatDate } = useFormatters(); const { refs, floatingStyles, context } = useFloating({ nodeId, @@ -54,42 +70,66 @@ const DateSelector = ({ value, onChange, unsetLabel, minDate, maxDate, reference ]); const defaultClassNames = getDefaultClassNames(); + const valueDate = value ? new Date(value) : undefined; + + const timeZoneLabel = new Intl.DateTimeFormat(undefined, { timeZoneName: "short" }) + .formatToParts(new Date()) + .find((p) => p.type === "timeZoneName")?.value; + + const today = new Date(); + const maxSelectable = maxDate && maxDate < today ? maxDate : today; + const disabledDays = [ + { after: maxSelectable }, + ...(minDate ? [{ before: minDate }] : []), + ]; + // Picking a day from the calendar keeps whatever time-of-day was already + // set (defaults to midnight for a brand-new value). function onDateSelect(date: Date | undefined) { if (!date) return; - const day = date.toISOString(); - onChange(day); - setIsOpen(false); + const next = new Date(date); + if (valueDate) { + next.setHours(valueDate.getHours(), valueDate.getMinutes(), 0, 0); + } else { + next.setHours(0, 0, 0, 0); + } + onChange(next.toISOString()); + } + + // Adjust just the time-of-day on the already-selected day. + function onTimeChange(e: React.ChangeEvent) { + if (!valueDate || !e.target.value) return; + const [hours, minutes] = e.target.value.split(":").map(Number); + const next = new Date(valueDate); + next.setHours(hours || 0, minutes || 0, 0, 0); + onChange(next.toISOString()); + } + + // Manually type (or pick via the native widget) the full date + time at once. + function onManualChange(e: React.ChangeEvent) { + if (!e.target.value) return; + const next = new Date(e.target.value); + if (isNaN(next.getTime())) return; + onChange(next.toISOString()); } - const valueDate = value ? new Date(value) : undefined; - // weird typescript issues const typefix: PropsSingle = { mode: "single", selected: valueDate, onSelect: onDateSelect, }; - const showDate = value && formatDate(value); - - const today = new Date(); - // Never allow future dates; also respect an optional upper bound (e.g. the - // chosen "before" date when picking "after", and vice-versa). - const maxSelectable = maxDate && maxDate < today ? maxDate : today; - const disabledDays = [ - { after: maxSelectable }, - ...(minDate ? [{ before: minDate }] : []), - ]; - // Show this field's own value if set, else the other field's month, else today. + const showValue = value && formatDisplay(valueDate as Date); const defaultMonth = valueDate || referenceDate || undefined; + return ( <> {isOpen && ( @@ -98,28 +138,59 @@ const DateSelector = ({ value, onChange, unsetLabel, minDate, maxDate, reference ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} - className='z-20 text-sm dark:bg-gray-700' + className='z-20 text-sm dark:bg-gray-700' > - +
+ +
+ + +
+
)} @@ -128,4 +199,4 @@ const DateSelector = ({ value, onChange, unsetLabel, minDate, maxDate, reference ); }; -export default DateSelector; +export default DateSelector; \ No newline at end of file