127 lines
3.9 KiB
JavaScript
127 lines
3.9 KiB
JavaScript
import ReactDatePicker from 'react-datepicker';
|
||
import { de } from 'date-fns/locale';
|
||
import { Calendar as CalendarIcon, Clock, X } from 'lucide-react';
|
||
import { forwardRef } from 'react';
|
||
import 'react-datepicker/dist/react-datepicker.css';
|
||
|
||
/**
|
||
* Custom DateTimePicker that reads the app's CSS variables and
|
||
* fully matches whatever theme is active.
|
||
*
|
||
* Props:
|
||
* value – ISO datetime string (or '')
|
||
* onChange – (isoString) => 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 selected = value ? new Date(value) : null;
|
||
|
||
const handleChange = (date) => {
|
||
if (!date) { onChange(''); return; }
|
||
// Produce local datetime string yyyy-MM-ddTHH:mm
|
||
const y = date.getFullYear();
|
||
const mo = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
const h = String(date.getHours()).padStart(2, '0');
|
||
const mi = String(date.getMinutes()).padStart(2, '0');
|
||
onChange(`${y}-${mo}-${d}T${h}:${mi}`);
|
||
};
|
||
|
||
const Icon = icon === 'clock' ? Clock : CalendarIcon;
|
||
|
||
return (
|
||
<div className="dtp-wrapper">
|
||
{label && (
|
||
<label className="block text-sm font-medium text-th-text mb-1.5">
|
||
{label}{required && ' *'}
|
||
</label>
|
||
)}
|
||
<div className="dtp-input-wrap">
|
||
<Icon size={15} className="dtp-icon" />
|
||
<ReactDatePicker
|
||
selected={selected}
|
||
onChange={handleChange}
|
||
showTimeSelect
|
||
timeFormat="HH:mm"
|
||
timeIntervals={15}
|
||
dateFormat="dd.MM.yyyy HH:mm"
|
||
locale={de}
|
||
minDate={minDate}
|
||
required={required}
|
||
popperPlacement="bottom-start"
|
||
popperModifiers={[
|
||
{ name: 'offset', options: { offset: [0, 4] } },
|
||
{ name: 'preventOverflow', options: { rootBoundary: 'viewport', tether: false, altAxis: true } },
|
||
]}
|
||
customInput={<CustomInput />}
|
||
calendarClassName="dtp-calendar"
|
||
dayClassName={(date) => 'dtp-day'}
|
||
timeClassName={() => 'dtp-time-item'}
|
||
renderCustomHeader={CalendarHeader}
|
||
isClearable
|
||
clearButtonClassName="dtp-clear-btn"
|
||
wrapperClassName="dtp-dp-wrapper"
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Custom Input ─────────────────────────────────────────────────────────────
|
||
const CustomInput = forwardRef(({ value, onClick, onClear, ...rest }, ref) => (
|
||
<button
|
||
ref={ref}
|
||
type="button"
|
||
onClick={onClick}
|
||
className="dtp-custom-input"
|
||
{...rest}
|
||
>
|
||
<span className={value ? 'dtp-value' : 'dtp-placeholder'}>
|
||
{value || 'Datum & Uhrzeit wählen …'}
|
||
</span>
|
||
</button>
|
||
));
|
||
CustomInput.displayName = 'CustomInput';
|
||
|
||
// ── Custom Header ─────────────────────────────────────────────────────────────
|
||
function CalendarHeader({
|
||
date,
|
||
decreaseMonth, increaseMonth,
|
||
prevMonthButtonDisabled, nextMonthButtonDisabled,
|
||
}) {
|
||
const label = date.toLocaleString('de', { month: 'long', year: 'numeric' });
|
||
return (
|
||
<div className="dtp-header">
|
||
<button
|
||
type="button"
|
||
onClick={decreaseMonth}
|
||
disabled={prevMonthButtonDisabled}
|
||
className="dtp-nav-btn"
|
||
aria-label="Vorheriger Monat"
|
||
>
|
||
‹
|
||
</button>
|
||
<span className="dtp-month-label">{label}</span>
|
||
<button
|
||
type="button"
|
||
onClick={increaseMonth}
|
||
disabled={nextMonthButtonDisabled}
|
||
className="dtp-nav-btn"
|
||
aria-label="Nächster Monat"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|