diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9ba7f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +server/public +vite.config.ts.* +*.tar.gz \ No newline at end of file diff --git a/.replit b/.replit new file mode 100644 index 0000000..43487c6 --- /dev/null +++ b/.replit @@ -0,0 +1,46 @@ +modules = ["nodejs-20", "web", "postgresql-16"] +run = "npm run dev" +hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] + +[nix] +channel = "stable-24_05" + +[deployment] +deploymentTarget = "autoscale" +build = ["npm", "run", "build"] +run = ["npm", "run", "start"] + +[[ports]] +localPort = 5000 +externalPort = 80 + +[[ports]] +localPort = 38607 +externalPort = 3000 + +[env] +PORT = "5000" + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Start application" + +[[workflows.workflow]] +name = "Start application" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "npm run dev" +waitForPort = 5000 + +[agent] +integrations = ["javascript_mem_db:1.0.0"] diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..31e4d45 --- /dev/null +++ b/client/index.html @@ -0,0 +1,18 @@ + + + + + TaskFlow - Personal Task Management + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..63deb8c --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,257 @@ +import { useState } from 'react'; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; + +// Components +import BottomNavigation from './components/BottomNavigation'; +import TaskCreationModal from './components/TaskCreationModal'; +import TaskList from './components/TaskList'; +import CalendarView from './components/CalendarView'; +import KanbanBoard from './components/KanbanBoard'; +import ProjectTemplate from './components/ProjectTemplate'; +import ThemeToggle from './components/ThemeToggle'; +import { Task } from './components/TaskCard'; +import { addDays, subDays } from 'date-fns'; + +// Mock data for prototype +const initialTasks: Task[] = [ + { + id: '1', + title: 'Review quarterly reports', + description: 'Analyze Q3 performance metrics and prepare summary', + status: 'todo', + priority: 'high', + dueDate: addDays(new Date(), 2), + timeTracked: 0, + isTracking: false, + projectId: 'project-1' + }, + { + id: '2', + title: 'Update mobile app interface', + description: 'Implement new design system and improve user experience', + status: 'inProgress', + priority: 'high', + dueDate: addDays(new Date(), 1), + timeTracked: 120, + isTracking: true, + projectId: 'project-2' + }, + { + id: '3', + title: 'Team standup meeting', + status: 'todo', + priority: 'low', + dueDate: new Date(), + timeTracked: 0, + isTracking: false + }, + { + id: '4', + title: 'Fix critical login bug', + description: 'Users unable to login with special characters in password', + status: 'todo', + priority: 'high', + dueDate: subDays(new Date(), 1), + timeTracked: 30, + isTracking: false, + projectId: 'project-2' + }, + { + id: '5', + title: 'Deploy new features', + status: 'done', + priority: 'medium', + dueDate: subDays(new Date(), 2), + timeTracked: 120, + isTracking: false, + projectId: 'project-1' + } +]; + +function App() { + const [currentTab, setCurrentTab] = useState('tasks'); + const [tasks, setTasks] = useState(initialTasks); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + const handleCreateTask = (newTask: Partial) => { + const task: Task = { + id: Date.now().toString(), + title: newTask.title || '', + description: newTask.description, + status: 'todo', + priority: newTask.priority || 'medium', + dueDate: newTask.dueDate, + timeTracked: 0, + isTracking: false, + projectId: newTask.projectId + }; + setTasks(prev => [...prev, task]); + console.log('Task created:', task); + }; + + const handleTaskUpdate = (taskId: string, updates: Partial) => { + setTasks(prev => prev.map(task => + task.id === taskId ? { ...task, ...updates } : task + )); + console.log('Task updated:', taskId, updates); + }; + + const handleTaskStatusChange = (taskId: string, newStatus: Task['status']) => { + handleTaskUpdate(taskId, { status: newStatus }); + }; + + const handleTaskDrop = (taskId: string, newDate: Date) => { + handleTaskUpdate(taskId, { dueDate: newDate }); + }; + + const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => { + console.log('Creating project from template:', { templateId, startDate, projectName }); + // In a real app, this would create multiple tasks based on the template + }; + + const renderContent = () => { + switch (currentTab) { + case 'tasks': + return ( + console.log('Edit task:', task.title)} + /> + ); + + case 'calendar': + return ( + console.log('Date selected:', date.toLocaleDateString())} + /> + ); + + case 'kanban': + return ( + + ); + + case 'templates': + return ( + + ); + + case 'settings': + return ( +
+
+

Settings

+ + +
+
+

Dark Mode

+

Toggle between light and dark themes

+
+ +
+
+ + +
+

About TaskFlow

+

+ A personal task management app with calendar scheduling, kanban boards, and project templates. +

+
+ Version 1.0.0 • Built with React and Tailwind CSS +
+
+
+
+
+ ); + + default: + return ( +
+

Page not found

+
+ ); + } + }; + + return ( + + +
+ {/* Header */} +
+
+
+
+ T +
+

+ TaskFlow +

+
+ +
+ {currentTab === 'templates' && ( + + )} + +
+
+
+ + {/* Main Content */} +
+ {renderContent()} +
+ + {/* Bottom Navigation */} + { + if (tab === 'templates') { + setCurrentTab('templates'); + } else { + setCurrentTab(tab); + } + }} + onCreateTask={() => setIsCreateModalOpen(true)} + /> + + {/* Task Creation Modal */} + setIsCreateModalOpen(false)} + onSave={handleCreateTask} + /> +
+ + +
+
+ ); +} + +export default App; diff --git a/client/src/components/BottomNavigation.tsx b/client/src/components/BottomNavigation.tsx new file mode 100644 index 0000000..f260fc7 --- /dev/null +++ b/client/src/components/BottomNavigation.tsx @@ -0,0 +1,80 @@ +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Home, Calendar, LayoutGrid, Plus, Settings } from 'lucide-react'; +import { useState } from 'react'; + +interface BottomNavigationProps { + onTabChange?: (tab: string) => void; + onCreateTask?: () => void; + activeTab?: string; +} + +export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) { + const [currentTab, setCurrentTab] = useState(activeTab); + + const tabs = [ + { id: 'tasks', label: 'Tasks', icon: Home, badge: 3 }, + { id: 'calendar', label: 'Calendar', icon: Calendar }, + { id: 'create', label: 'Create', icon: Plus, isCreate: true }, + { id: 'kanban', label: 'Board', icon: LayoutGrid }, + { id: 'templates', label: 'Templates', icon: Settings } + ]; + + const handleTabClick = (tabId: string, isCreate?: boolean) => { + if (isCreate) { + onCreateTask?.(); + console.log('Create task triggered'); + return; + } + + setCurrentTab(tabId); + onTabChange?.(tabId); + console.log('Tab changed to:', tabId); + }; + + return ( +
+
+ {tabs.map((tab) => { + const isActive = currentTab === tab.id && !tab.isCreate; + const Icon = tab.icon; + + return ( + + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/CalendarView.tsx b/client/src/components/CalendarView.tsx new file mode 100644 index 0000000..3cdb6b5 --- /dev/null +++ b/client/src/components/CalendarView.tsx @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'; +import { Task } from './TaskCard'; +import { addDays, format, isSameDay, startOfWeek } from 'date-fns'; + +interface CalendarViewProps { + tasks: Task[]; + onTaskDrop?: (taskId: string, newDate: Date) => void; + onDateSelect?: (date: Date) => void; +} + +export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: CalendarViewProps) { + const [currentDate, setCurrentDate] = useState(new Date()); + const [draggedTask, setDraggedTask] = useState(null); + + // Get two weeks starting from current week + const startDate = startOfWeek(currentDate); + const dates = Array.from({ length: 14 }, (_, i) => addDays(startDate, i)); + + const getTasksForDate = (date: Date) => { + return tasks.filter(task => + task.dueDate && isSameDay(task.dueDate, date) + ); + }; + + const handleDragStart = (taskId: string) => { + setDraggedTask(taskId); + console.log('Drag started for task:', taskId); + }; + + const handleDragEnd = () => { + setDraggedTask(null); + }; + + const handleDrop = (date: Date) => { + if (draggedTask) { + onTaskDrop?.(draggedTask, date); + console.log(`Task ${draggedTask} dropped on`, date.toLocaleDateString()); + setDraggedTask(null); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = addDays(currentDate, direction === 'next' ? 7 : -7); + setCurrentDate(newDate); + console.log(`Navigating to week of ${newDate.toLocaleDateString()}`); + }; + + return ( +
+ {/* Header */} +
+
+ +

+ Next Two Weeks +

+
+ +
+ + + +
+
+ + {/* Calendar Grid */} +
+ {dates.map((date, index) => { + const dayTasks = getTasksForDate(date); + const isToday = isSameDay(date, new Date()); + const isWeekend = date.getDay() === 0 || date.getDay() === 6; + + return ( + handleDrop(date)} + onClick={() => { + onDateSelect?.(date); + console.log('Date selected:', date.toLocaleDateString()); + }} + data-testid={`calendar-date-${format(date, 'yyyy-MM-dd')}`} + > +
+
+ {format(date, 'EEE')} +
+
+ {format(date, 'd')} +
+
+ +
+ {dayTasks.slice(0, 3).map((task, taskIndex) => ( +
handleDragStart(task.id)} + onDragEnd={handleDragEnd} + className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`} + data-testid={`calendar-task-${task.id}`} + > + +
+ {task.title} +
+
+
+ ))} + + {dayTasks.length > 3 && ( + + +{dayTasks.length - 3} more + + )} +
+
+ ); + })} +
+ + {/* Legend */} +
+
+
+ Today +
+
+
+ Weekend +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/KanbanBoard.tsx b/client/src/components/KanbanBoard.tsx new file mode 100644 index 0000000..2fb250e --- /dev/null +++ b/client/src/components/KanbanBoard.tsx @@ -0,0 +1,208 @@ +import { useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react'; +import { Task } from './TaskCard'; +import TaskCard from './TaskCard'; +import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns'; + +interface KanbanBoardProps { + tasks: Task[]; + onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void; + onTaskUpdate?: (taskId: string, updates: Partial) => void; +} + +type ViewMode = 'traditional' | 'weekly' | 'monthly'; + +export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }: KanbanBoardProps) { + const [viewMode, setViewMode] = useState('traditional'); + const [currentDate, setCurrentDate] = useState(new Date()); + const [draggedTask, setDraggedTask] = useState(null); + + const columns = [ + { id: 'todo', title: 'To Do', status: 'todo' as const }, + { id: 'inProgress', title: 'In Progress', status: 'inProgress' as const }, + { id: 'done', title: 'Done', status: 'done' as const } + ]; + + const getTasksForColumn = (status: Task['status']) => { + let filteredTasks = tasks.filter(task => task.status === status); + + if (viewMode === 'weekly') { + const weekStart = startOfWeek(currentDate); + const weekEnd = endOfWeek(currentDate); + filteredTasks = filteredTasks.filter(task => + task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd }) + ); + } else if (viewMode === 'monthly') { + const monthStart = startOfMonth(currentDate); + const monthEnd = endOfMonth(currentDate); + filteredTasks = filteredTasks.filter(task => + task.dueDate && isWithinInterval(task.dueDate, { start: monthStart, end: monthEnd }) + ); + } + + return filteredTasks; + }; + + const handleDragStart = (taskId: string) => { + setDraggedTask(taskId); + }; + + const handleDragEnd = () => { + setDraggedTask(null); + }; + + const handleDrop = (status: Task['status']) => { + if (draggedTask) { + onTaskStatusChange?.(draggedTask, status); + console.log(`Task ${draggedTask} moved to ${status}`); + setDraggedTask(null); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const navigatePeriod = (direction: 'prev' | 'next') => { + if (viewMode === 'weekly') { + setCurrentDate(addWeeks(currentDate, direction === 'next' ? 1 : -1)); + } else if (viewMode === 'monthly') { + setCurrentDate(addMonths(currentDate, direction === 'next' ? 1 : -1)); + } + }; + + const getPeriodTitle = () => { + if (viewMode === 'weekly') { + return `Week of ${format(startOfWeek(currentDate), 'MMM d')}`; + } else if (viewMode === 'monthly') { + return format(currentDate, 'MMMM yyyy'); + } + return 'All Tasks'; + }; + + return ( +
+ {/* Header */} +
+
+ +

+ Task Board +

+
+ + {viewMode !== 'traditional' && ( +
+ + + + {getPeriodTitle()} + + + +
+ )} +
+ + {/* View Mode Tabs */} + setViewMode(value as ViewMode)}> + + + + Traditional + + + + Weekly + + + + Monthly + + + + + {/* Kanban Columns */} +
+ {columns.map((column) => { + const columnTasks = getTasksForColumn(column.status); + + return ( + handleDrop(column.status)} + data-testid={`column-${column.id}`} + > +
+

+ {column.title} +

+ + {columnTasks.length} + +
+ +
+ {columnTasks.map((task) => ( +
handleDragStart(task.id)} + onDragEnd={handleDragEnd} + className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`} + > + { + onTaskUpdate?.(task.id, { isTracking: true }); + console.log(`Timer started for ${task.title}`); + }} + onPause={() => { + onTaskUpdate?.(task.id, { isTracking: false }); + console.log(`Timer paused for ${task.title}`); + }} + onEdit={() => { + console.log(`Edit task ${task.title}`); + }} + isDragging={draggedTask === task.id} + /> +
+ ))} + + {columnTasks.length === 0 && ( +
+ No tasks in {column.title.toLowerCase()} +
+ )} +
+
+ ); + })} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/ProjectTemplate.tsx b/client/src/components/ProjectTemplate.tsx new file mode 100644 index 0000000..8a01f76 --- /dev/null +++ b/client/src/components/ProjectTemplate.tsx @@ -0,0 +1,310 @@ +import { useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Calendar } from '@/components/ui/calendar'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Plus, Calendar as CalendarIcon, Clock, Copy, Play } from 'lucide-react'; +import { Task } from './TaskCard'; + +interface TemplateTask { + id: string; + title: string; + description?: string; + priority: 'low' | 'medium' | 'high'; + dayOffset: number; // Days relative to project start date (negative for before, positive for after) + estimatedHours?: number; +} + +interface ProjectTemplate { + id: string; + name: string; + description?: string; + tasks: TemplateTask[]; + category: string; +} + +interface ProjectTemplateProps { + templates?: ProjectTemplate[]; + onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void; + onCreateTemplate?: (template: Omit) => void; +} + +const defaultTemplates: ProjectTemplate[] = [ + { + id: 'web-project', + name: 'Website Launch', + description: 'Complete website development and launch process', + category: 'Development', + tasks: [ + { id: '1', title: 'Project planning', priority: 'high', dayOffset: -14, estimatedHours: 4 }, + { id: '2', title: 'Design mockups', priority: 'high', dayOffset: -10, estimatedHours: 16 }, + { id: '3', title: 'Frontend development', priority: 'high', dayOffset: -7, estimatedHours: 40 }, + { id: '4', title: 'Backend API', priority: 'medium', dayOffset: -5, estimatedHours: 24 }, + { id: '5', title: 'Testing and QA', priority: 'high', dayOffset: -2, estimatedHours: 8 }, + { id: '6', title: 'Deploy to production', priority: 'high', dayOffset: 0, estimatedHours: 2 } + ] + }, + { + id: 'product-launch', + name: 'Product Launch', + description: 'Marketing and launch campaign for new product', + category: 'Marketing', + tasks: [ + { id: '1', title: 'Market research', priority: 'high', dayOffset: -21, estimatedHours: 12 }, + { id: '2', title: 'Create marketing materials', priority: 'medium', dayOffset: -14, estimatedHours: 20 }, + { id: '3', title: 'Press release', priority: 'medium', dayOffset: -7, estimatedHours: 4 }, + { id: '4', title: 'Social media campaign', priority: 'high', dayOffset: -3, estimatedHours: 8 }, + { id: '5', title: 'Launch event', priority: 'high', dayOffset: 0, estimatedHours: 6 } + ] + } +]; + +export default function ProjectTemplate({ + templates = defaultTemplates, + onCreateFromTemplate, + onCreateTemplate +}: ProjectTemplateProps) { + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [projectName, setProjectName] = useState(''); + const [startDate, setStartDate] = useState(); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isLaunchOpen, setIsLaunchOpen] = useState(false); + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + + const handleLaunchProject = () => { + if (selectedTemplate && startDate && projectName.trim()) { + onCreateFromTemplate?.(selectedTemplate.id, startDate, projectName.trim()); + console.log('Launching project:', { + template: selectedTemplate.name, + name: projectName, + startDate: startDate.toLocaleDateString() + }); + + setProjectName(''); + setStartDate(undefined); + setSelectedTemplate(null); + setIsLaunchOpen(false); + } + }; + + const formatDayOffset = (offset: number) => { + if (offset === 0) return 'Launch day'; + if (offset < 0) return `${Math.abs(offset)} days before`; + return `${offset} days after`; + }; + + const getTotalEstimatedHours = (template: ProjectTemplate) => { + return template.tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); + }; + + const getTemplatesByCategory = () => { + const categories = Array.from(new Set(templates.map(t => t.category))); + return categories.map(category => ({ + category, + templates: templates.filter(t => t.category === category) + })); + }; + + return ( +
+ {/* Header */} +
+

+ Project Templates +

+ + + + + + + + Create New Template + +
+ Template creation form would go here +
+
+
+
+ + {/* Templates by Category */} + {getTemplatesByCategory().map(({ category, templates: categoryTemplates }) => ( +
+

+ {category} +

+ +
+ {categoryTemplates.map((template) => ( + +
+
+
+

+ {template.name} +

+ {template.description && ( +

+ {template.description} +

+ )} +
+ + + {template.tasks.length} tasks + +
+ +
+
+ + {getTotalEstimatedHours(template)}h estimated +
+
+ + {Math.abs(Math.min(...template.tasks.map(t => t.dayOffset)))} day timeline +
+
+ +
+
Sample tasks:
+
+ {template.tasks.slice(0, 3).map((task) => ( +
+ {task.title} + + {formatDayOffset(task.dayOffset)} + +
+ ))} + {template.tasks.length > 3 && ( +
+ +{template.tasks.length - 3} more tasks +
+ )} +
+
+ +
+ + + +
+
+
+ ))} +
+
+ ))} + + {/* Launch Project Dialog */} + + + + + + Launch Project from Template + + + + {selectedTemplate && ( +
+
+
{selectedTemplate.name}
+
+ {selectedTemplate.tasks.length} tasks • {getTotalEstimatedHours(selectedTemplate)}h estimated +
+
+ +
+ setProjectName(e.target.value)} + data-testid="input-project-name" + /> +
+ +
+ + + + + + { + setStartDate(date); + setIsCalendarOpen(false); + }} + disabled={(date) => date < new Date()} + initialFocus + /> + + +
+ +
+ + +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/TaskCard.tsx b/client/src/components/TaskCard.tsx new file mode 100644 index 0000000..9178727 --- /dev/null +++ b/client/src/components/TaskCard.tsx @@ -0,0 +1,148 @@ +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Clock, Calendar, Play, Pause, MoreHorizontal } from "lucide-react"; +import { useState } from "react"; + +export interface Task { + id: string; + title: string; + description?: string; + status: 'todo' | 'inProgress' | 'done'; + priority: 'low' | 'medium' | 'high'; + dueDate?: Date; + timeTracked: number; // in minutes + isTracking: boolean; + projectId?: string; + notes?: string; +} + +interface TaskCardProps { + task: Task; + onPlay?: () => void; + onPause?: () => void; + onEdit?: () => void; + onStatusChange?: (status: Task['status']) => void; + isDragging?: boolean; +} + +export default function TaskCard({ task, onPlay, onPause, onEdit, onStatusChange, isDragging }: TaskCardProps) { + const [isTimerRunning, setIsTimerRunning] = useState(task.isTracking); + + const handleToggleTimer = () => { + if (isTimerRunning) { + onPause?.(); + console.log(`Timer paused for task: ${task.title}`); + } else { + onPlay?.(); + console.log(`Timer started for task: ${task.title}`); + } + setIsTimerRunning(!isTimerRunning); + }; + + const getPriorityColor = (priority: string) => { + switch (priority) { + case 'high': return 'bg-destructive text-destructive-foreground'; + case 'medium': return 'bg-yellow-500 text-white'; + default: return 'bg-muted text-muted-foreground'; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'done': return 'bg-green-500 text-white'; + case 'inProgress': return 'bg-primary text-primary-foreground'; + default: return 'bg-muted text-muted-foreground'; + } + }; + + const formatTime = (minutes: number) => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${hours}h ${mins}m`; + }; + + return ( + +
+
+

+ {task.title} +

+ {task.description && ( +

+ {task.description} +

+ )} + +
+ + {task.priority} + + + + {task.status.replace(/([A-Z])/g, ' $1').toLowerCase()} + + + {task.dueDate && ( +
+ + + {task.dueDate.toLocaleDateString()} + +
+ )} +
+ + {task.timeTracked > 0 && ( +
+ + + {formatTime(task.timeTracked)} + +
+ )} +
+ +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/TaskCreationModal.tsx b/client/src/components/TaskCreationModal.tsx new file mode 100644 index 0000000..c45bb2a --- /dev/null +++ b/client/src/components/TaskCreationModal.tsx @@ -0,0 +1,152 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Calendar } from '@/components/ui/calendar'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { CalendarIcon, Plus } from 'lucide-react'; +import { Task } from './TaskCard'; + +interface TaskCreationModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (task: Partial) => void; +} + +export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium'); + const [dueDate, setDueDate] = useState(); + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + + const handleSave = () => { + if (!title.trim()) return; + + const newTask: Partial = { + title: title.trim(), + description: description.trim() || undefined, + priority, + dueDate, + status: 'todo', + timeTracked: 0, + isTracking: false + }; + + onSave(newTask); + console.log('New task created:', newTask); + + // Reset form + setTitle(''); + setDescription(''); + setPriority('medium'); + setDueDate(undefined); + onClose(); + }; + + const handleClose = () => { + setTitle(''); + setDescription(''); + setPriority('medium'); + setDueDate(undefined); + onClose(); + }; + + return ( + + + + + + New Task + + + +
+
+ setTitle(e.target.value)} + className="text-base" + data-testid="input-task-title" + autoFocus + /> +
+ +
+