EventBuzz_Console/src/app/components/ui/FlatpickrDatetimeInput.tsx

216 lines
5.7 KiB
TypeScript

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<K extends string>(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<HTMLInputElement>(null);
const fpRef = React.useRef<FlatpickrHandle | null>(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 (
<input
ref={inputRef}
type="text"
placeholder={placeholder}
readOnly={false}
autoComplete="off"
style={{
...style,
...(hasError ? { borderColor: "var(--destructive)" } : {}),
}}
/>
);
}
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<HTMLInputElement>(null);
const fpRef = React.useRef<FlatpickrHandle | null>(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 (
<input
ref={inputRef}
type="text"
placeholder={placeholder}
readOnly={false}
autoComplete="off"
style={{
...style,
...(hasError ? { borderColor: "var(--destructive)" } : {}),
}}
/>
);
}