From 5694df4adfd5f84bb2e232dd796c5c55780b615c Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Thu, 24 Jul 2025 08:07:52 +0300 Subject: [PATCH] Split screen --- frontend/App.tsx | 12 +- frontend/components/Task/TaskDetails.tsx | 544 ++++++++++++++++++ .../Task/TaskForm/TaskContentSection.tsx | 69 ++- frontend/components/Task/TaskItem.tsx | 61 +- frontend/components/Task/TaskModal.tsx | 148 ++--- 5 files changed, 659 insertions(+), 175 deletions(-) create mode 100644 frontend/components/Task/TaskDetails.tsx diff --git a/frontend/App.tsx b/frontend/App.tsx index 75295c8..b0ba589 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -17,7 +17,7 @@ import About from './components/About'; import Layout from './Layout'; import { User } from './entities/User'; import TasksToday from './components/Task/TasksToday'; -import TaskView from './components/Task/TaskView'; +import TaskDetails from './components/Task/TaskDetails'; import LoadingScreen from './components/Shared/LoadingScreen'; import InboxItems from './components/Inbox/InboxItems'; // Lazy load Tasks component to prevent issues with tags loading @@ -25,14 +25,13 @@ const Tasks = lazy(() => import('./components/Tasks')); const App: React.FC = () => { const { i18n } = useTranslation(); + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); if (!i18n.isInitialized) { return ; } - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); - const fetchCurrentUser = async () => { try { const response = await fetch('/api/current_user', { @@ -176,7 +175,10 @@ const App: React.FC = () => { element={} /> } /> - } /> + } + /> { + const { uuid } = useParams<{ uuid: string }>(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const { showSuccessToast, showErrorToast } = useToast(); + + const projects = useStore((state) => state.projectsStore.projects); + + // Local state + const [task, setTask] = useState(null); + const [subtasks, setSubtasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); + const [taskToDelete, setTaskToDelete] = useState(null); + const [isTimelineExpanded, setIsTimelineExpanded] = useState(false); + + // Date and recurrence formatting functions (from TaskHeader) + const formatDueDate = (dueDate: string) => { + const today = new Date().toISOString().split('T')[0]; + const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + if (dueDate === today) return t('dateIndicators.today', 'TODAY'); + if (dueDate === tomorrow) + return t('dateIndicators.tomorrow', 'TOMORROW'); + if (dueDate === yesterday) + return t('dateIndicators.yesterday', 'YESTERDAY'); + + return new Date(dueDate).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + const formatRecurrence = (recurrenceType: string) => { + switch (recurrenceType) { + case 'daily': + return t('recurrence.daily', 'Daily'); + case 'weekly': + return t('recurrence.weekly', 'Weekly'); + case 'monthly': + return t('recurrence.monthly', 'Monthly'); + case 'monthly_weekday': + return t('recurrence.monthlyWeekday', 'Monthly'); + case 'monthly_last_day': + return t('recurrence.monthlyLastDay', 'Monthly'); + default: + return t('recurrence.recurring', 'Recurring'); + } + }; + + useEffect(() => { + const fetchTaskData = async () => { + if (!uuid) { + setError('No task UUID provided'); + setLoading(false); + return; + } + + try { + setLoading(true); + const taskData = await fetchTaskByUuid(uuid); + setTask(taskData); + + // Load subtasks if this task has any + if (taskData.id) { + try { + const subtasksData = await fetchSubtasks(taskData.id); + setSubtasks(subtasksData); + } catch (subtaskError) { + console.warn('Error loading subtasks:', subtaskError); + // Don't fail the whole page if subtasks fail to load + } + } + } catch (fetchError) { + setError('Task not found'); + console.error('Error fetching task:', fetchError); + } finally { + setLoading(false); + } + }; + + fetchTaskData(); + }, [uuid]); + + const handleEdit = () => { + setIsTaskModalOpen(true); + }; + + const handleToggleCompletion = async () => { + if (!task?.id) return; + + try { + const updatedTask = await toggleTaskCompletion(task.id); + setTask(updatedTask); + + const statusMessage = + updatedTask.status === 'done' || updatedTask.status === 2 + ? t('task.completedSuccess', 'Task marked as completed') + : t('task.reopenedSuccess', 'Task reopened'); + + showSuccessToast(statusMessage); + } catch (error) { + console.error('Error toggling task completion:', error); + showErrorToast( + t('task.toggleError', 'Failed to update task status') + ); + } + }; + + const handleTaskUpdate = async (updatedTask: Task) => { + try { + if (task?.id) { + const updated = await updateTask(task.id, updatedTask); + setTask(updated); + showSuccessToast( + t('task.updateSuccess', 'Task updated successfully') + ); + + // Reload subtasks in case they changed + if (updated.id) { + try { + const subtasksData = await fetchSubtasks(updated.id); + setSubtasks(subtasksData); + } catch (subtaskError) { + console.warn('Error reloading subtasks:', subtaskError); + } + } + } + setIsTaskModalOpen(false); + } catch (error) { + console.error('Error updating task:', error); + showErrorToast(t('task.updateError', 'Failed to update task')); + } + }; + + const handleDeleteClick = () => { + if (task) { + setTaskToDelete(task); + setIsConfirmDialogOpen(true); + } + }; + + const handleDeleteConfirm = async () => { + if (taskToDelete?.id) { + try { + await deleteTask(taskToDelete.id); + showSuccessToast( + t('task.deleteSuccess', 'Task deleted successfully') + ); + navigate('/today'); // Navigate back to today view after deletion + } catch (error) { + console.error('Error deleting task:', error); + showErrorToast(t('task.deleteError', 'Failed to delete task')); + } + } + setIsConfirmDialogOpen(false); + setTaskToDelete(null); + }; + + const handleCreateProject = async (name: string): Promise => { + try { + return await createProject({ name }); + } catch (error) { + console.error('Error creating project:', error); + throw error; + } + }; + + const handleSubtaskClick = (subtask: Task) => { + if (subtask.uuid) { + navigate(`/task/${subtask.uuid}`); + } + }; + + if (loading) { + return ; + } + + if (error || !task) { + return ( +
+
+
+ +

+ {error || t('task.notFound', 'Task Not Found')} +

+

+ {t( + 'task.notFoundDescription', + 'The task you are looking for does not exist or has been deleted.' + )} +

+ +
+
+
+ ); + } + + return ( +
+
+ {/* Header Section with Title and Action Buttons */} +
+
+ +
+

+ {task.name} +

+ {/* Project, tags, due date, and recurrence under title */} + {(task.Project || + (task.tags && task.tags.length > 0) || + task.due_date || + (task.recurrence_type && + task.recurrence_type !== 'none')) && ( +
+ {task.Project && ( +
+ + + {task.Project.name} + +
+ )} + {task.Project && + task.tags && + task.tags.length > 0 && ( + + )} + {task.tags && task.tags.length > 0 && ( +
+ + + {task.tags.map((tag, index) => ( + + + {tag.name} + + {index < + task.tags!.length - + 1 && ', '} + + ))} + +
+ )} + {(task.Project || + (task.tags && task.tags.length > 0)) && + task.due_date && ( + + )} + {task.due_date && ( +
+ + + {formatDueDate(task.due_date)} + +
+ )} + {(task.Project || + (task.tags && task.tags.length > 0) || + task.due_date) && + task.recurrence_type && + task.recurrence_type !== 'none' && ( + + )} + {task.recurrence_type && + task.recurrence_type !== 'none' && ( +
+ + + {formatRecurrence( + task.recurrence_type + )} + +
+ )} +
+ )} +
+
+
+ + + +
+
+ + {/* Content - Two column layout for notes and subtasks */} + {(task.note || subtasks.length > 0) && ( +
+
+ {/* Notes Column */} + {task.note && ( +
+

+ {t('task.notes', 'Notes')} +

+
+ +
+
+ )} + + {/* Subtasks Column */} + {subtasks.length > 0 && ( +
+

+ {t('task.subtasks', 'Subtasks')} +

+
+ {subtasks.map((subtask) => ( +
+
+ handleSubtaskClick( + subtask + ) + } + className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 cursor-pointer transition-all duration-200 ${ + subtask.status === + 'in_progress' || + subtask.status === 1 + ? 'border-green-400/60 dark:border-green-500/60' + : 'border-gray-50 dark:border-gray-800' + }`} + > +
+
+ { + if ( + subtask.id + ) { + try { + await toggleTaskCompletion( + subtask.id + ); + // Reload subtasks after toggling completion + if ( + task?.id + ) { + const subtasksData = + await fetchSubtasks( + task.id + ); + setSubtasks( + subtasksData + ); + } + } catch (error) { + console.error( + 'Error toggling subtask completion:', + error + ); + } + } + }} + /> +
+ + {subtask.name} + +
+
+
+ ))} +
+
+ )} +
+
+ )} + + {/* Activity Timeline */} + {isTimelineExpanded && task?.id && ( +
+ + setIsTimelineExpanded(!isTimelineExpanded) + } + /> +
+ )} + + {/* Task Modal for Editing */} + setIsTaskModalOpen(false)} + onSave={handleTaskUpdate} + onDelete={async (taskId: number) => { + await deleteTask(taskId); + navigate('/today'); + }} + projects={projects} + onCreateProject={handleCreateProject} + showToast={false} + initialSubtasks={subtasks} + /> + + {/* Confirm Delete Dialog */} + {isConfirmDialogOpen && taskToDelete && ( + { + setIsConfirmDialogOpen(false); + setTaskToDelete(null); + }} + /> + )} +
+
+ ); +}; + +export default TaskDetails; diff --git a/frontend/components/Task/TaskForm/TaskContentSection.tsx b/frontend/components/Task/TaskForm/TaskContentSection.tsx index ae8edc1..1f062c9 100644 --- a/frontend/components/Task/TaskForm/TaskContentSection.tsx +++ b/frontend/components/Task/TaskForm/TaskContentSection.tsx @@ -1,5 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { EyeIcon, PencilIcon } from '@heroicons/react/24/outline'; +import MarkdownRenderer from '../../Shared/MarkdownRenderer'; interface TaskContentSectionProps { taskId: number | undefined; @@ -13,20 +15,65 @@ const TaskContentSection: React.FC = ({ onChange, }) => { const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState<'edit' | 'preview'>('edit'); return (
-