Files
redlight/src/components/DateTimePicker.jsx
T
Michelle 4028e913c4
Build & Push Docker Image / build (push) Successful in 4m21s
refactor: update class names for consistency and improve styling
- Changed `flex-shrink-0` to `shrink-0` in multiple components for better consistency.
- Updated button and checkbox classes to use `rounded-sm` for a more uniform appearance.
- Adjusted backdrop blur classes for modals to `backdrop-blur-xs` for a subtler effect.
- Removed unused Tailwind CSS configuration file.
2026-05-18 13:07:26 +02:00

106 lines
3.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react';
import flatpickr from 'flatpickr';
import 'flatpickr/dist/flatpickr.min.css';
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 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
* icon 'calendar' (default) | 'clock'
*/
export default function DateTimePicker({
value,
onChange,
label,
required = false,
minDate = null,
icon = 'calendar',
}) {
const inputRef = useRef(null);
const fpRef = useRef(null);
// Always keep a current ref to onChange so flatpickr's closure never goes stale
const onChangeRef = useRef(onChange);
useEffect(() => { onChangeRef.current = onChange; });
useEffect(() => {
if (!inputRef.current) return;
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) { onChangeRef.current(''); 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');
onChangeRef.current(`${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;
return (
<div>
{label && (
<label className="block text-sm font-medium text-th-text mb-1.5">
{label}{required && ' *'}
</label>
)}
<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>
);
}