'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; interface Props { value: string; // "YYYY-MM-DD" onChange: (value: string) => void; className?: string; } const MONTH_NAMES = [ 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', ]; const WEEKDAY_SHORT = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; function pad(n: number): string { return String(n).padStart(2, '0'); } function monthKey(date: Date): string { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; } function monthLabel(ym: string): string { const [ys, ms] = ym.split('-'); const y = Number(ys); const m = Number(ms); return `${MONTH_NAMES[m - 1]} ${y}`; } function shiftMonth(ym: string, delta: number): string { const [ys, ms] = ym.split('-'); const d = new Date(Number(ys), Number(ms) - 1 + delta, 1); return monthKey(d); } function parseISODate(v: string): Date | null { if (!isValidDateString(v)) return null; const [ys, ms, ds] = v.split('-'); return new Date(Number(ys), Number(ms) - 1, Number(ds)); } function buildMonthGrid(ym: string): Array { const [ys, ms] = ym.split('-'); const y = Number(ys); const m = Number(ms); const first = new Date(y, m - 1, 1); const days = new Date(y, m, 0).getDate(); const mondayStartOffset = (first.getDay() + 6) % 7; const cells: Array = Array(mondayStartOffset).fill(null); for (let d = 1; d <= days; d += 1) cells.push(d); while (cells.length % 7 !== 0) cells.push(null); return cells; } function isValidDateString(v: string): boolean { if (!/^\d{4}-\d{2}-\d{2}$/.test(v)) return false; const [ys, ms, ds] = v.split('-'); const y = Number(ys); const m = Number(ms); const d = Number(ds); if (m < 1 || m > 12 || d < 1 || d > 31) return false; const dt = new Date(Date.UTC(y, m - 1, d)); return ( dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d ); } function normalize(v: string): string { const digits = v.replace(/\D/g, '').slice(0, 8); const y = digits.slice(0, 4); const m = digits.slice(4, 6); const d = digits.slice(6, 8); if (digits.length <= 4) return y; if (digits.length <= 6) return `${y}-${m}`; return `${y}-${m}-${d}`; } export default function DateInput({ value, onChange, className = '' }: Props) { const [local, setLocal] = useState(value); const [error, setError] = useState(false); const [open, setOpen] = useState(false); const [viewMonth, setViewMonth] = useState(monthKey(new Date())); const wrapperRef = useRef(null); const selectedDate = useMemo(() => parseISODate(value), [value]); const dayCells = useMemo(() => buildMonthGrid(viewMonth), [viewMonth]); useEffect(() => { setLocal(value); setError(false); }, [value]); useEffect(() => { const date = parseISODate(value); if (date) setViewMonth(monthKey(date)); }, [value]); useEffect(() => { function onDocMouseDown(ev: MouseEvent) { if (!wrapperRef.current) return; if (!wrapperRef.current.contains(ev.target as Node)) setOpen(false); } function onDocKeyDown(ev: KeyboardEvent) { if (ev.key === 'Escape') setOpen(false); } document.addEventListener('mousedown', onDocMouseDown); document.addEventListener('keydown', onDocKeyDown); return () => { document.removeEventListener('mousedown', onDocMouseDown); document.removeEventListener('keydown', onDocKeyDown); }; }, []); function handleChange(raw: string) { setError(false); setLocal(normalize(raw)); } function handleBlur() { if (local === '') { setLocal(value); setError(false); return; } if (isValidDateString(local)) { setError(false); onChange(local); return; } setError(true); } function selectDay(day: number) { const [ys, ms] = viewMonth.split('-'); const iso = `${ys}-${ms}-${pad(day)}`; setLocal(iso); setError(false); onChange(iso); setOpen(false); } function isSelected(day: number): boolean { if (!selectedDate) return false; const [ys, ms] = viewMonth.split('-').map(Number); return selectedDate.getFullYear() === ys && selectedDate.getMonth() === ms - 1 && selectedDate.getDate() === day; } return (
handleChange(e.target.value)} onBlur={handleBlur} placeholder="YYYY-MM-DD" maxLength={10} className={`w-full px-2 py-1 border-2 rounded-lg bg-white text-sm text-gray-900 font-mono text-center focus:outline-none ${ error ? 'border-red-500 focus:border-red-500' : 'border-gray-400 focus:border-blue-500' }`} />
{open && (
{monthLabel(viewMonth)}
{WEEKDAY_SHORT.map((wd) => (
{wd}
))}
{dayCells.map((day, idx) => ( ))}
)} {error && (

Ungueltiges Datum (YYYY-MM-DD)

)}
); }