import React, { useState, useRef, useEffect } from 'react' import ReactDOM from 'react-dom' import { Calendar, Clock, ChevronLeft, ChevronRight, ChevronUp, ChevronDown } from 'lucide-react' import { useTranslation } from '../../i18n' function daysInMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate() } function getWeekday(year: number, month: number, day: number): number { return new Date(year, month, day).getDay() } interface CustomDatePickerProps { value: string onChange: (value: string) => void placeholder?: string style?: React.CSSProperties compact?: boolean borderless?: boolean } export function CustomDatePicker({ value, onChange, placeholder, style = {}, compact = false, borderless = false }: CustomDatePickerProps) { const { locale, t } = useTranslation() const [open, setOpen] = useState(false) const ref = useRef(null) const dropRef = useRef(null) const parsed = value ? new Date(value + 'T00:00:00Z') : null const [viewYear, setViewYear] = useState(parsed?.getUTCFullYear() || new Date().getFullYear()) const [viewMonth, setViewMonth] = useState(parsed?.getUTCMonth() ?? new Date().getMonth()) useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current?.contains(e.target as Node)) return if (dropRef.current?.contains(e.target as Node)) return setOpen(false) } if (open) document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [open]) useEffect(() => { if (open && parsed) { setViewYear(parsed.getUTCFullYear()); setViewMonth(parsed.getUTCMonth()) } }, [open]) const prevMonth = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) } else setViewMonth(m => m - 1) } const nextMonth = () => { if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1) } else setViewMonth(m => m + 1) } const monthLabel = new Date(viewYear, viewMonth).toLocaleDateString(locale, { month: 'long', year: 'numeric' }) const days = daysInMonth(viewYear, viewMonth) const startDay = (getWeekday(viewYear, viewMonth, 1) + 6) % 7 const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i + 1).toLocaleDateString(locale, { weekday: 'narrow' })) const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: 'UTC' } : { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) : null const selectDay = (day: number) => { const y = String(viewYear) const m = String(viewMonth + 1).padStart(2, '0') const d = String(day).padStart(2, '0') onChange(`${y}-${m}-${d}`) setOpen(false) } const selectedDay = parsed && parsed.getUTCFullYear() === viewYear && parsed.getUTCMonth() === viewMonth ? parsed.getUTCDate() : null const today = new Date() const isToday = (d: number) => today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d const [textInput, setTextInput] = useState('') const [isTyping, setIsTyping] = useState(false) const handleTextSubmit = () => { setIsTyping(false) if (!textInput.trim()) return // Try to parse various date formats const input = textInput.trim() // ISO: 2026-03-29 if (/^\d{4}-\d{2}-\d{2}$/.test(input)) { onChange(input); return } // EU: 29.03.2026 or 29/03/2026 const euMatch = input.match(/^(\d{1,2})[./](\d{1,2})[./](\d{2,4})$/) if (euMatch) { const y = euMatch[3].length === 2 ? 2000 + parseInt(euMatch[3]) : parseInt(euMatch[3]) onChange(`${y}-${String(euMatch[2]).padStart(2, '0')}-${String(euMatch[1]).padStart(2, '0')}`) return } // Try native Date parse as fallback const d = new Date(input) if (!isNaN(d.getTime())) { onChange(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`) } } return (
{isTyping ? ( setTextInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleTextSubmit(); if (e.key === 'Escape') setIsTyping(false) }} onBlur={handleTextSubmit} placeholder="DD.MM.YYYY" style={{ width: '100%', padding: '8px 14px', borderRadius: 10, border: '1px solid var(--text-faint)', background: 'var(--bg-input)', color: 'var(--text-primary)', fontSize: 13, fontFamily: 'inherit', outline: 'none', }} /> ) : ( )} {open && ReactDOM.createPortal(
{ const r = ref.current?.getBoundingClientRect() if (!r) return { top: 0, left: 0 } const w = 268, pad = 8, h = 360 const vw = window.innerWidth const vh = window.visualViewport?.height ?? window.innerHeight let left = r.left let top = r.bottom + 4 if (left + w > vw - pad) left = Math.max(pad, vw - w - pad) if (top + h > vh - pad) top = r.top - h - 4 top = Math.max(pad, Math.min(top, vh - h - pad)) if (vw < 360) left = Math.max(pad, (vw - w) / 2) return { top, left } })(), zIndex: 99999, background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 14, boxShadow: '0 8px 32px rgba(0,0,0,0.12)', padding: 12, width: 268, maxWidth: 'calc(100vw - 16px)', animation: 'selectIn 0.15s ease-out', backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)', }}> {/* Month nav */}
{monthLabel}
{/* Weekday headers */}
{weekdays.map((d, i) => (
{d}
))}
{/* Days grid */}
{Array.from({ length: startDay }, (_, i) =>
)} {Array.from({ length: days }, (_, i) => { const d = i + 1 const sel = d === selectedDay const td = isToday(d) return ( ) })}
{/* Clear */} {value && (
)}
, document.body )}
) } interface CustomDateTimePickerProps { value: string onChange: (value: string) => void placeholder?: string style?: React.CSSProperties } export function CustomDateTimePicker({ value, onChange, placeholder, style = {} }: CustomDateTimePickerProps) { const { locale } = useTranslation() const [datePart, timePart] = (value || '').split('T') const handleDateChange = (d: string) => { onChange(d ? `${d}T${timePart || '12:00'}` : '') } const handleTimeChange = (t: string) => { const d = datePart || new Date().toISOString().split('T')[0] onChange(t ? `${d}T${t}` : d) } return (
) } import CustomTimePicker from './CustomTimePicker'