import * as React from "react"; import flatpickr from "flatpickr"; import "flatpickr/dist/flatpickr.min.css"; /** Subset of flatpickr instance API we use (avoids tight coupling to type export paths). */ interface FlatpickrHandle { destroy(): void; readonly selectedDates: Date[]; setDate(date: Date | string, triggerChange?: boolean, formatStr?: string): void; clear(triggerChange?: boolean): void; set(key: K, value: unknown): void; input: HTMLInputElement; } function pad2(n: number) { return String(n).padStart(2, "0"); } /** Same string shape previously used for `datetime-local` inputs (local wall time). */ export function toDatetimeLocalValue(isoOrDate: string | Date | undefined): string { if (!isoOrDate) return ""; const d = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate; if (Number.isNaN(d.getTime())) return ""; return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`; } function parseDatetimeLocal(s: string): Date | undefined { if (!s.trim()) return undefined; const d = new Date(s); return Number.isNaN(d.getTime()) ? undefined : d; } function parseDateOnly(s: string): Date | undefined { const raw = s.trim(); if (!raw) return undefined; if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { const [y, m, d] = raw.split("-").map(Number); const date = new Date(y, m - 1, d); return Number.isNaN(date.getTime()) ? undefined : date; } const d = new Date(raw); return Number.isNaN(d.getTime()) ? undefined : d; } /** Local calendar date as `YYYY-MM-DD`. */ export function toDateOnlyValue(isoOrDate: string | Date | undefined): string { if (!isoOrDate) return ""; const d = typeof isoOrDate === "string" ? parseDateOnly(isoOrDate) : isoOrDate; if (!d || Number.isNaN(d.getTime())) return ""; return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } export interface FlatpickrDateInputProps { value: string; onChange: (value: string) => void; style?: React.CSSProperties; disabled?: boolean; placeholder?: string; hasError?: boolean; } /** Date-only picker (Flatpickr) that reads/writes `YYYY-MM-DD`. */ export function FlatpickrDateInput({ value, onChange, style, disabled, placeholder = "Select date…", hasError, }: FlatpickrDateInputProps) { const inputRef = React.useRef(null); const fpRef = React.useRef(null); React.useEffect(() => { const el = inputRef.current; if (!el) return; const fp = flatpickr(el, { enableTime: false, dateFormat: "Y-m-d", allowInput: true, defaultDate: parseDateOnly(value), onChange: dates => { const d = dates[0]; onChange(d ? toDateOnlyValue(d) : ""); }, }) as FlatpickrHandle; fpRef.current = fp; return () => { fp.destroy(); fpRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps -- init once }, []); React.useEffect(() => { const fp = fpRef.current; if (!fp) return; const selected = fp.selectedDates[0]; const cur = selected ? toDateOnlyValue(selected) : ""; if (value === cur) return; const d = parseDateOnly(value); if (d) fp.setDate(d, false); else fp.clear(false); }, [value]); React.useEffect(() => { const fp = fpRef.current; if (!fp) return; const input = fp.input as HTMLInputElement; input.disabled = !!disabled; fp.set("clickOpens", !disabled); }, [disabled]); return ( ); } export interface FlatpickrDatetimeInputProps { value: string; onChange: (value: string) => void; style?: React.CSSProperties; disabled?: boolean; placeholder?: string; hasError?: boolean; } /** * Date + time picker (Flatpickr) that reads/writes the same `YYYY-MM-DDTHH:mm` local values * as native `datetime-local` inputs. */ export function FlatpickrDatetimeInput({ value, onChange, style, disabled, placeholder = "Select date & time…", hasError, }: FlatpickrDatetimeInputProps) { const inputRef = React.useRef(null); const fpRef = React.useRef(null); React.useEffect(() => { const el = inputRef.current; if (!el) return; const fp = flatpickr(el, { enableTime: true, time_24hr: true, dateFormat: "Y-m-d H:i", allowInput: true, defaultDate: parseDatetimeLocal(value), onChange: dates => { const d = dates[0]; onChange(d ? toDatetimeLocalValue(d) : ""); }, }) as FlatpickrHandle; fpRef.current = fp; return () => { fp.destroy(); fpRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps -- init once }, []); React.useEffect(() => { const fp = fpRef.current; if (!fp) return; const selected = fp.selectedDates[0]; const cur = selected ? toDatetimeLocalValue(selected) : ""; if (value === cur) return; const d = parseDatetimeLocal(value); if (d) fp.setDate(d, false); else fp.clear(false); }, [value]); React.useEffect(() => { const fp = fpRef.current; if (!fp) return; const input = fp.input as HTMLInputElement; input.disabled = !!disabled; fp.set("clickOpens", !disabled); }, [disabled]); return ( ); }