import React, { useState, useRef, useEffect, Suspense } from "react"; import dynamic from "next/dynamic"; const RichTextEditor = dynamic(() => import("./RichTextEditor"), { ssr: false }); import { Repeat, X, ChevronDown, Link } from "lucide-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons"; import { faServer, faFolder, IconDefinition } from "@fortawesome/free-solid-svg-icons"; import MdiIcon from "@mdi/react"; import { allIcons } from "./iconRegistry"; import { Task, KanbanStage, getPriorityBadge } from "./WeeklyView"; interface GridTaskBlockProps { task: Task; date: Date; activeDate: Date; cellDuration: number; darkMode: boolean; isProtected: boolean; editingTaskId: string | null; setEditingTaskId: (id: string | null) => void; updateTask: (id: string, title: string) => void; updateTaskNotes: (id: string, notes: string) => void; updateTaskUrl: (id: string, url: string) => void; updateTaskDuration: (id: string, duration: number) => void; toggleTask: (id: string) => void; deleteTask: (id: string) => void; toggleTaskRolling: (id: string) => void; setSelectedTaskForNotes: (task: Task) => void; setSelectedTaskForRecurrence: (task: Task) => void; handleDragStart: (e: React.DragEvent, task: Task) => void; handleDragEnd: (e: React.DragEvent) => void; getSlotHeight: (duration: number) => number; draggedTask: Task | null; addSubTask: (parentId: string, title: string) => void; toggleSubTask: (id: string) => void; updateSubTask: (id: string, title: string) => void; deleteSubTask: (id: string) => void; onSetEditingTaskId?: (id: string | null) => void; workingHoursStart: number; showTaskCheckboxes?: boolean; showProjectIcons?: boolean; showPriorityIcons?: boolean; priorityStyle?: string; projects?: any[]; onProjectAssign?: (taskId: string, projectId: string | null) => void; kanbanStages?: KanbanStage[]; weatherEnabled?: boolean; onMoveTask?: (taskId: string, targetDate: Date, targetTime: string) => void; } export function GridTaskBlock({ task, date, activeDate, cellDuration, darkMode, isProtected, editingTaskId, setEditingTaskId, updateTask, updateTaskNotes, updateTaskUrl, updateTaskDuration, toggleTask, deleteTask, toggleTaskRolling, setSelectedTaskForNotes, setSelectedTaskForRecurrence, handleDragStart, handleDragEnd, getSlotHeight, draggedTask, addSubTask, toggleSubTask, updateSubTask, deleteSubTask, onSetEditingTaskId, workingHoursStart, showTaskCheckboxes, showProjectIcons, showPriorityIcons = true, priorityStyle = "eisenhower", projects, onProjectAssign, kanbanStages = [], weatherEnabled = false, onMoveTask, }: GridTaskBlockProps) { const [isNotesOpen, setIsNotesOpen] = useState(false); const [notesValue, setNotesValue] = useState(task.markdownContent || ""); const [isUrlOpen, setIsUrlOpen] = useState(false); const [urlValue, setUrlValue] = useState(task.url || ""); const urlInputRef = useRef(null); const [isSubTasksOpen, setIsSubTasksOpen] = useState(false); const [isSubTaskInputOpen, setIsSubTaskInputOpen] = useState(false); const [newSubTaskTitle, setNewSubTaskTitle] = useState(""); const [showProjectPicker, setShowProjectPicker] = useState(false); const [showMoveDialog, setShowMoveDialog] = useState(false); const [moveDate, setMoveDate] = useState(''); const [moveTime, setMoveTime] = useState(''); const notesRef = useRef(null); const subTaskInputRef = useRef(null); const projectPickerRef = useRef(null); const moveDialogRef = useRef(null); // Touch: tap-to-reveal actions const [touchActive, setTouchActive] = useState(false); const blockRef = useRef(null); useEffect(() => { if (!touchActive) return; const handler = (e: Event) => { if (blockRef.current && !blockRef.current.contains(e.target as Node)) setTouchActive(false); }; document.addEventListener("touchstart", handler); document.addEventListener("mousedown", handler); return () => { document.removeEventListener("touchstart", handler); document.removeEventListener("mousedown", handler); }; }, [touchActive]); // Close project picker on outside click useEffect(() => { if (!showProjectPicker) return; const handler = (e: Event) => { if (projectPickerRef.current && !projectPickerRef.current.contains(e.target as Node)) { setShowProjectPicker(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [showProjectPicker]); // Resize State const [isResizing, setIsResizing] = useState(false); const [resizeHeight, setResizeHeight] = useState(null); const resizeStartY = useRef(0); const resizeStartHeight = useRef(0); // Focus notes when opened useEffect(() => { if (isNotesOpen && notesRef.current) { notesRef.current.focus(); } }, [isNotesOpen]); // Focus URL input when opened useEffect(() => { if (isUrlOpen) setTimeout(() => urlInputRef.current?.focus(), 50); }, [isUrlOpen]); // Sync urlValue if task.url changes externally useEffect(() => { setUrlValue(task.url || ""); }, [task.url]); const handleNotesBlur = () => { if (notesValue !== task.markdownContent) { updateTaskNotes(task.id, notesValue); } }; const handleUrlSave = (val: string) => { setIsUrlOpen(false); if (val.trim() !== (task.url || "")) { updateTaskUrl(task.id, val.trim()); } }; const insertMarkdown = (prefix: string, suffix: string = "") => { if (!notesRef.current) return; const start = notesRef.current.selectionStart; const end = notesRef.current.selectionEnd; const text = notesValue; const before = text.substring(0, start); const selection = text.substring(start, end); const after = text.substring(end); const newText = `${before}${prefix}${selection}${suffix}${after}`; setNotesValue(newText); setTimeout(() => { if (notesRef.current) { notesRef.current.focus(); const newCursorPos = start + prefix.length + selection.length + suffix.length; notesRef.current.setSelectionRange(newCursorPos, newCursorPos); } }, 0); }; const pixelsPerMinute = getSlotHeight(cellDuration) / cellDuration; useEffect(() => { if (!task.startTime) return; const handleResizeMove = (e: MouseEvent) => { if (!isResizing) return; const deltaY = e.clientY - resizeStartY.current; let newHeight = resizeStartHeight.current + deltaY; const minHeight = 15 * pixelsPerMinute; if (newHeight < minHeight) newHeight = minHeight; setResizeHeight(newHeight); }; const handleResizeEnd = () => { if (!isResizing) return; setIsResizing(false); document.body.style.cursor = ""; if (resizeHeight !== null) { const newDurationMins = Math.round(resizeHeight / pixelsPerMinute / 15) * 15; updateTaskDuration(task.id, newDurationMins); } setResizeHeight(null); }; if (isResizing) { window.addEventListener("mousemove", handleResizeMove); window.addEventListener("mouseup", handleResizeEnd); } return () => { window.removeEventListener("mousemove", handleResizeMove); window.removeEventListener("mouseup", handleResizeEnd); }; }, [isResizing, resizeHeight, pixelsPerMinute, task.id, updateTaskDuration, task.startTime]); // Conditional rendering should only happen after hooks if (!task.startTime) return null; const [startHour, startMinute] = task.startTime.split(":").map(Number); const startMinutes = startHour * 60 + startMinute; const topOffset = startMinutes * pixelsPerMinute; const duration = task.duration || 15; const baseHeight = duration * pixelsPerMinute; const currentHeight = isResizing && resizeHeight !== null ? resizeHeight : baseHeight; const onResizeStart = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); setIsResizing(true); resizeStartY.current = e.clientY; resizeStartHeight.current = baseHeight; document.body.style.cursor = "ns-resize"; }; const onResizeKeyDown = (e: React.KeyboardEvent) => { if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; e.preventDefault(); e.stopPropagation(); const step = 15; const newDuration = e.key === 'ArrowUp' ? Math.min(duration + step, 480) : Math.max(duration - step, 15); if (newDuration !== duration) { updateTaskDuration(task.id, newDuration); } }; return (
{ const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null; if (stageColor) return `4px solid ${stageColor}`; if (task.project?.color) return `4px solid ${task.project.color}`; return (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none"; })(), borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0", padding: (task.kanbanStage && kanbanStages.some(s => s.id === task.kanbanStage)) || task.project?.color ? "2px 4px 2px 8px" : "2px 4px", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", display: "flex", flexDirection: "column", overflow: "visible !important", }} draggable={!editingTaskId && !isResizing} onDragStart={(e) => handleDragStart(e, task)} onDragEnd={handleDragEnd} onMouseLeave={(e) => { if (showProjectPicker) { // Don't close if mouse moved into the project picker dropdown const related = e.relatedTarget as HTMLElement | null; if (related && projectPickerRef.current?.contains(related)) return; setShowProjectPicker(false); } }} onClick={(e) => { e.stopPropagation(); // Touch: toggle action toolbar on tap instead of toggling completion if (window.matchMedia("(pointer: coarse)").matches && editingTaskId !== task.id) { const target = e.target as HTMLElement; if (target.closest(".task-actions") || target.closest("button")) return; setTouchActive(!touchActive); return; } if (editingTaskId !== task.id) toggleTask(task.id); }} >
{editingTaskId === task.id ? (
{ e.preventDefault(); const input = e.currentTarget.elements.namedItem("title") as HTMLTextAreaElement; updateTask(task.id, input.value || ""); }} onClick={(e) => e.stopPropagation()} style={{ width: "100%", paddingRight: "20px" }} >