Files
redlight/src/components/DateTimePicker.jsx
Michelle fcb83a9b72
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m58s
feat(DateTimePicker): implement custom popper container for improved layout handling
feat(Modal): enhance modal styling with rounded corners and improved overflow handling
style: adjust z-index for datepicker popper to ensure proper layering above modals
2026-03-04 10:49:17 +01:00

131 lines
4.2 KiB
JavaScript
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 ReactDatePicker from 'react-datepicker';
import { de } from 'date-fns/locale';
import { Calendar as CalendarIcon, Clock } from 'lucide-react';
import { forwardRef } from 'react';
import { createPortal } from 'react-dom';
import 'react-datepicker/dist/react-datepicker.css';
// Renders the calendar popper into document.body so it never affects the form
// layout (prevents the "everything shifts down + auto-scroll" bug).
const PopperContainer = ({ children }) => createPortal(children, document.body);
/**
* 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"
popperContainer={PopperContainer}
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}
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>
);
}