From b25ea537abc18d662cd55909a73677d8be9ba216 Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Fri, 4 Jul 2025 23:44:20 +0300 Subject: [PATCH] Add a datepicker and a days selector --- frontend/components/Shared/DatePicker.tsx | 316 ++++++++++++++++++ .../Shared/NumberSelectDropdown.tsx | 146 ++++++++ .../components/Shared/PriorityDropdown.tsx | 35 +- .../components/Shared/RecurrenceDropdown.tsx | 35 +- .../Shared/RecurrenceSelectDropdown.tsx | 143 ++++++++ frontend/components/Shared/ToggleSwitch.tsx | 80 +++++ frontend/components/Task/RecurrenceInput.tsx | 220 ++++++------ .../Task/TaskForm/TaskMetadataSection.tsx | 21 +- frontend/components/Task/TaskModal.tsx | 15 +- 9 files changed, 879 insertions(+), 132 deletions(-) create mode 100644 frontend/components/Shared/DatePicker.tsx create mode 100644 frontend/components/Shared/NumberSelectDropdown.tsx create mode 100644 frontend/components/Shared/RecurrenceSelectDropdown.tsx create mode 100644 frontend/components/Shared/ToggleSwitch.tsx diff --git a/frontend/components/Shared/DatePicker.tsx b/frontend/components/Shared/DatePicker.tsx new file mode 100644 index 0000000..085b75f --- /dev/null +++ b/frontend/components/Shared/DatePicker.tsx @@ -0,0 +1,316 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CalendarDaysIcon } from '@heroicons/react/24/outline'; + +interface DatePickerProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; + className?: string; + label?: string; +} + +const DatePicker: React.FC = ({ + value, + onChange, + placeholder = 'Select date', + disabled = false, + className = '', + label +}) => { + const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false }); + const [currentMonth, setCurrentMonth] = useState(new Date()); + const dropdownRef = useRef(null); + const menuRef = useRef(null); + + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + const formatDate = (date: Date) => { + return date.toISOString().split('T')[0]; + }; + + const parseDate = (dateString: string) => { + return dateString ? new Date(dateString + 'T00:00:00') : null; + }; + + const formatDisplayDate = (dateString: string) => { + if (!dateString) return placeholder; + const date = parseDate(dateString); + if (!date || isNaN(date.getTime())) return placeholder; + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const handleToggle = () => { + if (disabled) return; + + if (!isOpen && dropdownRef.current) { + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const menuHeight = 320; // Calendar height + const padding = 16; // Extra padding from viewport edges + + // Determine if we should open upward + const wouldFitBelow = spaceBelow >= menuHeight + padding; + const wouldFitAbove = spaceAbove >= menuHeight + padding; + + let openUpward = false; + let top = rect.bottom + 8; + + if (!wouldFitBelow && wouldFitAbove) { + // Open upward if it fits above but not below + openUpward = true; + top = rect.top - menuHeight - 8; + } else if (!wouldFitBelow && !wouldFitAbove) { + // If it doesn't fit in either direction, choose the side with more space + if (spaceAbove > spaceBelow) { + openUpward = true; + top = Math.max(padding, rect.top - menuHeight - 8); + } else { + top = Math.min(window.innerHeight - menuHeight - padding, rect.bottom + 8); + } + } + + // Ensure left position doesn't go off screen + const left = Math.min( + Math.max(padding, rect.left), + window.innerWidth - Math.max(rect.width, 280) - padding + ); + + setPosition({ + top, + left, + width: Math.max(rect.width, 280), // Minimum width for calendar + openUpward + }); + + // Set current month based on selected date or today + if (value) { + const selectedDate = parseDate(value); + if (selectedDate && !isNaN(selectedDate.getTime())) { + setCurrentMonth(new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1)); + } + } else { + setCurrentMonth(new Date(new Date().getFullYear(), new Date().getMonth(), 1)); + } + } + setIsOpen(!isOpen); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && + menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + const handleDateSelect = (date: Date) => { + try { + onChange(formatDate(date)); + setIsOpen(false); + } catch (error) { + console.error('Error in date selection:', error); + setIsOpen(false); + } + }; + + const handleClear = (e: React.MouseEvent) => { + e.stopPropagation(); + onChange(''); + setIsOpen(false); + }; + + const navigateMonth = (direction: 'prev' | 'next') => { + setCurrentMonth(prev => { + const newMonth = new Date(prev); + if (direction === 'prev') { + newMonth.setMonth(newMonth.getMonth() - 1); + } else { + newMonth.setMonth(newMonth.getMonth() + 1); + } + return newMonth; + }); + }; + + const getDaysInMonth = () => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + const startingDayOfWeek = firstDay.getDay(); + + const days = []; + + // Add empty cells for days before the first day of the month + for (let i = 0; i < startingDayOfWeek; i++) { + days.push(null); + } + + // Add days of the month + for (let day = 1; day <= daysInMonth; day++) { + days.push(new Date(year, month, day)); + } + + return days; + }; + + const isToday = (date: Date) => { + const today = new Date(); + return date.toDateString() === today.toDateString(); + }; + + const isSelected = (date: Date) => { + if (!value) return false; + const selectedDate = parseDate(value); + return selectedDate && date.toDateString() === selectedDate.toDateString(); + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + return ( +
+ + )} + +
+ + + {isOpen && createPortal( +
e.stopPropagation()} + > + {/* Calendar Header */} +
+ + + {months[currentMonth.getMonth()]} {currentMonth.getFullYear()} + + +
+ + {/* Calendar Grid */} +
+ {/* Day Headers */} +
+ {days.map(day => ( +
+ {day} +
+ ))} +
+ + {/* Calendar Days */} +
+ {getDaysInMonth().map((date, index) => ( +
+ {date && ( + + )} +
+ ))} +
+
+ + {/* Footer */} +
+ + {value && ( + + )} +
+
, + document.body + )} + + ); +}; + +export default DatePicker; \ No newline at end of file diff --git a/frontend/components/Shared/NumberSelectDropdown.tsx b/frontend/components/Shared/NumberSelectDropdown.tsx new file mode 100644 index 0000000..bb2145c --- /dev/null +++ b/frontend/components/Shared/NumberSelectDropdown.tsx @@ -0,0 +1,146 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; + +interface NumberSelectDropdownProps { + value: number; + onChange: (value: number) => void; + min?: number; + max?: number; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +const NumberSelectDropdown: React.FC = ({ + value, + onChange, + min = 1, + max = 99, + placeholder = 'Select number', + disabled = false, + className = '' +}) => { + const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false }); + const dropdownRef = useRef(null); + const menuRef = useRef(null); + + // Generate options from min to max + const options = Array.from({ length: max - min + 1 }, (_, i) => { + const num = min + i; + return { value: num, label: num.toString() }; + }); + + const handleToggle = () => { + if (disabled) return; + + if (!isOpen && dropdownRef.current) { + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const menuHeight = Math.min(options.length * 40 + 16, 200); // Max height with scroll + + const openUpward = spaceAbove > spaceBelow && spaceBelow < menuHeight; + + setPosition({ + top: openUpward ? rect.top - menuHeight - 8 : rect.bottom + 8, + left: rect.left, + width: rect.width, + openUpward + }); + } + setIsOpen(!isOpen); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && + menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + const handleSelect = (selectedValue: number) => { + try { + onChange(selectedValue); + setIsOpen(false); + } catch (error) { + console.error('Error in number dropdown selection:', error); + setIsOpen(false); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const selectedOption = options.find(option => option.value === value); + + return ( +
+ + + {isOpen && createPortal( +
e.stopPropagation()} + > + {options.map((option) => ( + + ))} +
, + document.body + )} +
+ ); +}; + +export default NumberSelectDropdown; \ No newline at end of file diff --git a/frontend/components/Shared/PriorityDropdown.tsx b/frontend/components/Shared/PriorityDropdown.tsx index f18ac00..fa44fe7 100644 --- a/frontend/components/Shared/PriorityDropdown.tsx +++ b/frontend/components/Shared/PriorityDropdown.tsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { ChevronDownIcon, ArrowDownIcon, ArrowUpIcon, FireIcon } from '@heroicons/react/24/outline'; import { PriorityType } from '../../entities/Task'; import { useTranslation } from 'react-i18next'; @@ -17,9 +18,26 @@ const PriorityDropdown: React.FC = ({ value, onChange }) { value: 'high', label: t('priority.high', 'High'), icon: } ]; const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false }); const dropdownRef = useRef(null); + const menuRef = useRef(null); const handleToggle = () => { + if (!isOpen && dropdownRef.current) { + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const menuHeight = 120; + + const openUpward = spaceAbove > spaceBelow && spaceBelow < menuHeight; + + setPosition({ + top: openUpward ? rect.top - menuHeight - 8 : rect.bottom + 8, + left: rect.left, + width: rect.width, + openUpward + }); + } setIsOpen(!isOpen); }; @@ -62,20 +80,29 @@ const PriorityDropdown: React.FC = ({ value, onChange }) - {isOpen && ( -
+ {isOpen && createPortal( +
{priorities.map((priority) => ( ))} -
+
, + document.body )} ); diff --git a/frontend/components/Shared/RecurrenceDropdown.tsx b/frontend/components/Shared/RecurrenceDropdown.tsx index 952d75e..d980f0e 100644 --- a/frontend/components/Shared/RecurrenceDropdown.tsx +++ b/frontend/components/Shared/RecurrenceDropdown.tsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { ChevronDownIcon, ArrowPathIcon, CalendarDaysIcon, ClockIcon } from '@heroicons/react/24/outline'; import { RecurrenceType } from '../../entities/Task'; import { useTranslation } from 'react-i18next'; @@ -21,9 +22,26 @@ const RecurrenceDropdown: React.FC = ({ value, onChange ]; const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false }); const dropdownRef = useRef(null); + const menuRef = useRef(null); const handleToggle = () => { + if (!isOpen && dropdownRef.current) { + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const menuHeight = 240; + + const openUpward = spaceAbove > spaceBelow && spaceBelow < menuHeight; + + setPosition({ + top: openUpward ? rect.top - menuHeight - 8 : rect.bottom + 8, + left: rect.left, + width: rect.width, + openUpward + }); + } setIsOpen(!isOpen); }; @@ -66,20 +84,29 @@ const RecurrenceDropdown: React.FC = ({ value, onChange - {isOpen && ( -
+ {isOpen && createPortal( +
{recurrenceOptions.map((recurrence) => ( ))} -
+
, + document.body )} ); diff --git a/frontend/components/Shared/RecurrenceSelectDropdown.tsx b/frontend/components/Shared/RecurrenceSelectDropdown.tsx new file mode 100644 index 0000000..1d20a5d --- /dev/null +++ b/frontend/components/Shared/RecurrenceSelectDropdown.tsx @@ -0,0 +1,143 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; + +interface Option { + value: string | number; + label: string; +} + +interface RecurrenceSelectDropdownProps { + value: string | number; + onChange: (value: string | number) => void; + options: Option[]; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +const RecurrenceSelectDropdown: React.FC = ({ + value, + onChange, + options, + placeholder = 'Select option', + disabled = false, + className = '' +}) => { + const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false }); + const dropdownRef = useRef(null); + const menuRef = useRef(null); + + const handleToggle = () => { + if (disabled) return; + + if (!isOpen && dropdownRef.current) { + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const menuHeight = Math.min(options.length * 40 + 16, 200); // Max height with scroll + + const openUpward = spaceAbove > spaceBelow && spaceBelow < menuHeight; + + setPosition({ + top: openUpward ? rect.top - menuHeight - 8 : rect.bottom + 8, + left: rect.left, + width: rect.width, + openUpward + }); + } + setIsOpen(!isOpen); + }; + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && + menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + const handleSelect = (selectedValue: string | number) => { + try { + onChange(selectedValue); + setIsOpen(false); + } catch (error) { + console.error('Error in dropdown selection:', error); + setIsOpen(false); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const selectedOption = options.find(option => option.value === value); + + return ( +
+ + + {isOpen && createPortal( +
e.stopPropagation()} + > + {options.map((option) => ( + + ))} +
, + document.body + )} +
+ ); +}; + +export default RecurrenceSelectDropdown; \ No newline at end of file diff --git a/frontend/components/Shared/ToggleSwitch.tsx b/frontend/components/Shared/ToggleSwitch.tsx new file mode 100644 index 0000000..0a1472d --- /dev/null +++ b/frontend/components/Shared/ToggleSwitch.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +interface ToggleSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + label: string; + description?: string; + disabled?: boolean; + className?: string; +} + +const ToggleSwitch: React.FC = ({ + checked, + onChange, + label, + description, + disabled = false, + className = '' +}) => { + const handleToggle = () => { + if (!disabled) { + onChange(!checked); + } + }; + + return ( +
+ + +
+ + {description && ( +

+ {description} +

+ )} +
+
+ ); +}; + +export default ToggleSwitch; \ No newline at end of file diff --git a/frontend/components/Task/RecurrenceInput.tsx b/frontend/components/Task/RecurrenceInput.tsx index 97cff65..9853b2e 100644 --- a/frontend/components/Task/RecurrenceInput.tsx +++ b/frontend/components/Task/RecurrenceInput.tsx @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { RecurrenceType } from '../../entities/Task'; import { useTranslation } from 'react-i18next'; +import RecurrenceSelectDropdown from '../Shared/RecurrenceSelectDropdown'; +import NumberSelectDropdown from '../Shared/NumberSelectDropdown'; +import ToggleSwitch from '../Shared/ToggleSwitch'; +import DatePicker from '../Shared/DatePicker'; interface RecurrenceInputProps { recurrenceType: RecurrenceType; @@ -54,71 +58,83 @@ const RecurrenceInput: React.FC = ({ { value: 5, label: t('recurrence.lastWeek', 'Last') }, ]; + const recurrenceTypeOptions = [ + { value: 'none', label: t('recurrence.none', 'No repeat') }, + { value: 'daily', label: t('recurrence.daily', 'Daily') }, + { value: 'weekly', label: t('recurrence.weekly', 'Weekly') }, + { value: 'monthly', label: t('recurrence.monthly', 'Monthly') }, + { value: 'monthly_weekday', label: t('recurrence.monthlyWeekday', 'Monthly on weekday') }, + { value: 'monthly_last_day', label: t('recurrence.monthlyLastDay', 'Monthly on last day') } + ]; + const renderRecurrenceTypeSelect = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => (
- + />
); - const renderIntervalInput = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => ( -
- -
- (customOnChange || onChange)('recurrence_interval', parseInt(e.target.value))} - className="block w-20 border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" + const renderIntervalInput = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => { + // Determine max value based on recurrence type + const getMaxValue = () => { + if (recurrenceType === 'daily') return 30; + if (recurrenceType === 'weekly') return 52; // Max 52 weeks (1 year) + if (recurrenceType === 'monthly' || recurrenceType === 'monthly_weekday' || recurrenceType === 'monthly_last_day') return 24; // Max 24 months (2 years) + return 99; + }; + + return ( +
+ +
+
+ (customOnChange || onChange)('recurrence_interval', value)} + min={1} + max={getMaxValue()} + disabled={isDisabled} + /> +
+ + {recurrenceType === 'daily' && t('recurrence.days', 'days')} + {recurrenceType === 'weekly' && t('recurrence.weeks', 'weeks')} + {(recurrenceType === 'monthly' || recurrenceType === 'monthly_weekday' || recurrenceType === 'monthly_last_day') && t('recurrence.months', 'months')} + +
+
+ ); + }; + + const renderWeekdaySelect = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => { + const weekdayOptions = [ + { value: '', label: t('recurrence.anyDay', 'Any day') }, + ...weekdays + ]; + + return ( +
+ + (customOnChange || onChange)('recurrence_weekday', value !== '' ? parseInt(value as string) : null)} + options={weekdayOptions} disabled={isDisabled} /> - - {recurrenceType === 'daily' && t('recurrence.days', 'days')} - {recurrenceType === 'weekly' && t('recurrence.weeks', 'weeks')} - {(recurrenceType === 'monthly' || recurrenceType === 'monthly_weekday' || recurrenceType === 'monthly_last_day') && t('recurrence.months', 'months')} -
-
- ); - - const renderWeekdaySelect = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => ( -
- - -
- ); + ); + }; const renderMonthDayInput = (customOnChange?: (field: string, value: any) => void, isDisabled?: boolean) => (
@@ -144,33 +160,21 @@ const RecurrenceInput: React.FC = ({ - + onChange={(value) => onChange('recurrence_week_of_month', parseInt(value as string))} + options={weekOfMonthOptions} + />
- + onChange={(value) => onChange('recurrence_weekday', parseInt(value as string))} + options={weekdays} + />
); @@ -180,11 +184,10 @@ const RecurrenceInput: React.FC = ({ - (customOnChange || onChange)('recurrence_end_date', e.target.value || null)} - className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" + onChange={(value) => (customOnChange || onChange)('recurrence_end_date', value || null)} + placeholder={t('forms.task.endDatePlaceholder', 'Select end date')} disabled={isDisabled} /> @@ -192,20 +195,12 @@ const RecurrenceInput: React.FC = ({ const renderCompletionBasedToggle = () => (
- -

- {t('forms.task.completionBasedHelp', 'If checked, the next task will be created based on completion date instead of due date')} -

+ onChange('completion_based', checked)} + label={t('forms.task.labels.completionBased', 'Repeat after completion')} + description={t('forms.task.completionBasedHelp', 'If checked, the next task will be created based on completion date instead of due date')} + />
); @@ -294,19 +289,12 @@ const RecurrenceInput: React.FC = ({ - + /> {(recurrenceType === 'daily' || recurrenceType === 'weekly' || recurrenceType === 'monthly' || recurrenceType === 'monthly_weekday' || recurrenceType === 'monthly_last_day') && ( @@ -315,15 +303,20 @@ const RecurrenceInput: React.FC = ({ {t('forms.task.labels.recurrenceInterval', 'Every')}
- onChange('recurrence_interval', parseInt(e.target.value))} - className="block w-20 border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" - disabled={disabled} - /> +
+ onChange('recurrence_interval', value)} + min={1} + max={ + recurrenceType === 'daily' ? 30 : + recurrenceType === 'weekly' ? 52 : + (recurrenceType === 'monthly' || recurrenceType === 'monthly_weekday' || recurrenceType === 'monthly_last_day') ? 24 : + 99 + } + disabled={disabled} + /> +
{recurrenceType === 'daily' && t('recurrence.days', 'days')} {recurrenceType === 'weekly' && t('recurrence.weeks', 'weeks')} @@ -337,11 +330,10 @@ const RecurrenceInput: React.FC = ({ - onChange('recurrence_end_date', e.target.value || null)} - className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" + onChange={(value) => onChange('recurrence_end_date', value || null)} + placeholder={t('forms.task.endDatePlaceholder', 'Select end date')} disabled={disabled} />
diff --git a/frontend/components/Task/TaskForm/TaskMetadataSection.tsx b/frontend/components/Task/TaskForm/TaskMetadataSection.tsx index e053f12..2238ef3 100644 --- a/frontend/components/Task/TaskForm/TaskMetadataSection.tsx +++ b/frontend/components/Task/TaskForm/TaskMetadataSection.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { PriorityType, StatusType } from '../../../entities/Task'; import StatusDropdown from '../../Shared/StatusDropdown'; import PriorityDropdown from '../../Shared/PriorityDropdown'; +import DatePicker from '../../Shared/DatePicker'; interface TaskMetadataSectionProps { priority: PriorityType; @@ -24,8 +25,8 @@ const TaskMetadataSection: React.FC = ({ const { t } = useTranslation(); return ( -
-
+
+
@@ -34,17 +35,19 @@ const TaskMetadataSection: React.FC = ({ onChange={onPriorityChange} />
-
+
- { + const event = { + target: { name: 'due_date', value } + } as React.ChangeEvent; + onDueDateChange(event); + }} + placeholder={t('forms.task.dueDatePlaceholder', 'Select due date')} />
diff --git a/frontend/components/Task/TaskModal.tsx b/frontend/components/Task/TaskModal.tsx index 18060a1..a5f878a 100644 --- a/frontend/components/Task/TaskModal.tsx +++ b/frontend/components/Task/TaskModal.tsx @@ -290,6 +290,19 @@ const TaskModal: React.FC = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + + // Ignore clicks on dropdown menus rendered via portal + if (target && ( + target.closest('.recurrence-dropdown-menu') || + target.closest('.number-dropdown-menu') || + target.closest('.date-picker-menu') || + target.closest('[class*="fixed z-50"]') || + target.closest('[class*="z-50"]') + )) { + return; + } + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { handleClose(); } @@ -396,7 +409,7 @@ const TaskModal: React.FC = ({ )} {expandedSections.metadata && ( -
+

{t('forms.task.statusAndOptions', 'Status & Options')}