diff --git a/package-lock.json b/package-lock.json index 6f30c4b..5d794ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,10 @@ "better-sqlite3": "^11.0.0", "concurrently": "^9.0.0", "cors": "^2.8.5", - "date-fns": "^4.1.0", "dotenv": "^16.4.0", "express": "^4.21.0", "express-rate-limit": "^7.5.1", + "flatpickr": "^4.6.13", "ioredis": "^5.10.0", "jsonwebtoken": "^9.0.0", "lucide-react": "^0.460.0", @@ -25,7 +25,6 @@ "pg": "^8.18.0", "rate-limit-redis": "^4.3.1", "react": "^18.3.0", - "react-datepicker": "^9.1.0", "react-dom": "^18.3.0", "react-hot-toast": "^2.4.0", "react-router-dom": "^6.28.0", @@ -779,59 +778,6 @@ "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.27.19", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", - "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -1895,15 +1841,6 @@ "node": ">=12" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2071,16 +2008,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2542,6 +2469,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -3955,27 +3888,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-datepicker": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", - "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.27.15", - "clsx": "^2.1.1", - "date-fns": "^4.1.0" - }, - "peerDependencies": { - "date-fns-tz": "^3.0.0", - "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "date-fns-tz": { - "optional": true - } - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -4605,12 +4517,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", diff --git a/package.json b/package.json index 6e4d06d..7115725 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "better-sqlite3": "^11.0.0", "concurrently": "^9.0.0", "cors": "^2.8.5", - "date-fns": "^4.1.0", "dotenv": "^16.4.0", "express": "^4.21.0", "express-rate-limit": "^7.5.1", + "flatpickr": "^4.6.13", "ioredis": "^5.10.0", "jsonwebtoken": "^9.0.0", "lucide-react": "^0.460.0", @@ -29,7 +29,6 @@ "pg": "^8.18.0", "rate-limit-redis": "^4.3.1", "react": "^18.3.0", - "react-datepicker": "^9.1.0", "react-dom": "^18.3.0", "react-hot-toast": "^2.4.0", "react-router-dom": "^6.28.0", diff --git a/src/components/DateTimePicker.jsx b/src/components/DateTimePicker.jsx index 03fa11e..a9ce35f 100644 --- a/src/components/DateTimePicker.jsx +++ b/src/components/DateTimePicker.jsx @@ -1,16 +1,22 @@ +import { useEffect, useRef } from 'react'; +import flatpickr from 'flatpickr'; +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 native + . - * No popups, no Popper.js, no scroll issues — the browser-native date/time - * pickers handle everything and respect the OS dark/light mode via color-scheme. + * 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 (only the date part is enforced) + * minDate – Date | null * icon – 'calendar' (default) | 'clock' */ export default function DateTimePicker({ @@ -21,28 +27,57 @@ export default function DateTimePicker({ minDate = null, icon = 'calendar', }) { - // Split 'YYYY-MM-DDTHH:mm' into date and time parts - const [datePart, timePart] = value ? value.split('T') : ['', '']; + const inputRef = useRef(null); + const fpRef = useRef(null); - const handleDateChange = (e) => { - const d = e.target.value; // YYYY-MM-DD - const t = timePart || '09:00'; - onChange(d ? `${d}T${t}` : ''); - }; + useEffect(() => { + if (!inputRef.current) return; - const handleTimeChange = (e) => { - const t = e.target.value; // HH:mm - const d = datePart || new Date().toISOString().slice(0, 10); - onChange(t ? `${d}T${t}` : ''); - }; + 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) { onChange(''); 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'); + onChange(`${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; - // Format minDate to YYYY-MM-DD for the native input - const minDateStr = minDate - ? `${minDate.getFullYear()}-${String(minDate.getMonth() + 1).padStart(2, '0')}-${String(minDate.getDate()).padStart(2, '0')}` - : undefined; - return (