feat(DateTimePicker): integrate flatpickr for enhanced date/time selection and theming
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import flatpickr from 'flatpickr';
|
||||
import { German } from 'flatpickr/dist/l10n/de.js';
|
||||
import { Calendar as CalendarIcon, Clock } from 'lucide-react';
|
||||
|
||||
// Register German as default locale
|
||||
flatpickr.localize(German);
|
||||
|
||||
/**
|
||||
* Themed DateTimePicker using native <input type="date"> + <input type="time">.
|
||||
* No popups, no Popper.js, no scroll issues — the browser-native date/time
|
||||
* pickers handle everything and respect the OS dark/light mode via color-scheme.
|
||||
* Themed DateTimePicker using flatpickr.
|
||||
* flatpickr uses position:fixed for its calendar dropdown — no overflow,
|
||||
* no scroll issues, no Popper.js needed. CSS variables drive all theming.
|
||||
*
|
||||
* Props:
|
||||
* value – local datetime string 'YYYY-MM-DDTHH:mm' (or '')
|
||||
* onChange – (localDatetimeString) => void
|
||||
* label – string
|
||||
* required – bool
|
||||
* minDate – Date | null (only the date part is enforced)
|
||||
* minDate – Date | null
|
||||
* icon – 'calendar' (default) | 'clock'
|
||||
*/
|
||||
export default function DateTimePicker({
|
||||
@@ -21,28 +27,57 @@ export default function DateTimePicker({
|
||||
minDate = null,
|
||||
icon = 'calendar',
|
||||
}) {
|
||||
// Split 'YYYY-MM-DDTHH:mm' into date and time parts
|
||||
const [datePart, timePart] = value ? value.split('T') : ['', ''];
|
||||
const inputRef = useRef(null);
|
||||
const fpRef = useRef(null);
|
||||
|
||||
const handleDateChange = (e) => {
|
||||
const d = e.target.value; // YYYY-MM-DD
|
||||
const t = timePart || '09:00';
|
||||
onChange(d ? `${d}T${t}` : '');
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return;
|
||||
|
||||
const handleTimeChange = (e) => {
|
||||
const t = e.target.value; // HH:mm
|
||||
const d = datePart || new Date().toISOString().slice(0, 10);
|
||||
onChange(t ? `${d}T${t}` : '');
|
||||
};
|
||||
fpRef.current = flatpickr(inputRef.current, {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
dateFormat: 'd.m.Y H:i',
|
||||
minuteIncrement: 15,
|
||||
minDate: minDate || undefined,
|
||||
defaultDate: value || undefined,
|
||||
appendTo: document.body, // portal to body → never clipped
|
||||
static: false,
|
||||
onChange: (selectedDates) => {
|
||||
if (selectedDates.length === 0) { onChange(''); return; }
|
||||
const d = selectedDates[0];
|
||||
const y = d.getFullYear();
|
||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const mi = String(d.getMinutes()).padStart(2, '0');
|
||||
onChange(`${y}-${mo}-${day}T${h}:${mi}`);
|
||||
},
|
||||
});
|
||||
|
||||
return () => fpRef.current?.destroy();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Sync value from outside
|
||||
useEffect(() => {
|
||||
if (!fpRef.current) return;
|
||||
const current = fpRef.current.selectedDates[0];
|
||||
const incoming = value ? new Date(value) : null;
|
||||
// Only setDate if actually different (avoid loops)
|
||||
if (incoming && (!current || Math.abs(incoming - current) > 60000)) {
|
||||
fpRef.current.setDate(incoming, false);
|
||||
} else if (!incoming && current) {
|
||||
fpRef.current.clear(false);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// Sync minDate
|
||||
useEffect(() => {
|
||||
if (!fpRef.current) return;
|
||||
fpRef.current.set('minDate', minDate || undefined);
|
||||
}, [minDate]);
|
||||
|
||||
const Icon = icon === 'clock' ? Clock : CalendarIcon;
|
||||
|
||||
// Format minDate to YYYY-MM-DD for the native input
|
||||
const minDateStr = minDate
|
||||
? `${minDate.getFullYear()}-${String(minDate.getMonth() + 1).padStart(2, '0')}-${String(minDate.getDate()).padStart(2, '0')}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
@@ -50,28 +85,16 @@ export default function DateTimePicker({
|
||||
{label}{required && ' *'}
|
||||
</label>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<CalendarIcon size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none" />
|
||||
<input
|
||||
type="date"
|
||||
value={datePart}
|
||||
onChange={handleDateChange}
|
||||
min={minDateStr}
|
||||
required={required}
|
||||
className="input-field pl-8 text-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-[7rem]">
|
||||
<Clock size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none" />
|
||||
<input
|
||||
type="time"
|
||||
value={timePart}
|
||||
onChange={handleTimeChange}
|
||||
required={required}
|
||||
className="input-field pl-8 text-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Icon size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none z-[1]" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
required={required}
|
||||
readOnly
|
||||
placeholder="Datum & Uhrzeit wählen…"
|
||||
className="input-field pl-9 text-sm w-full cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user