Build & Push Docker Image / build (push) Successful in 4m21s
- 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.
106 lines
3.3 KiB
React
106 lines
3.3 KiB
React
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>
|
||
);
|
||
}
|