From 28ad3f1535d2ece6a19ad43fb8e0c41699943614 Mon Sep 17 00:00:00 2001 From: paul-nothaft <40865108-paul-nothaft@users.noreply.replit.com> Date: Thu, 11 Sep 2025 08:59:20 +0000 Subject: [PATCH] Add core components for task management and navigation This commit introduces the foundational UI components and logic for the task management application, including task creation, calendar views, Kanban boards, and navigation elements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/yy9YLEW --- .gitignore | 6 + .replit | 46 + client/index.html | 18 + client/src/App.tsx | 257 + client/src/components/BottomNavigation.tsx | 80 + client/src/components/CalendarView.tsx | 169 + client/src/components/KanbanBoard.tsx | 208 + client/src/components/ProjectTemplate.tsx | 310 + client/src/components/TaskCard.tsx | 148 + client/src/components/TaskCreationModal.tsx | 152 + client/src/components/TaskList.tsx | 188 + client/src/components/ThemeToggle.tsx | 54 + .../components/examples/BottomNavigation.tsx | 17 + .../src/components/examples/CalendarView.tsx | 50 + .../src/components/examples/KanbanBoard.tsx | 75 + .../components/examples/ProjectTemplate.tsx | 9 + client/src/components/examples/TaskCard.tsx | 33 + .../components/examples/TaskCreationModal.tsx | 29 + client/src/components/examples/TaskList.tsx | 77 + .../src/components/examples/ThemeToggle.tsx | 19 + client/src/components/ui/accordion.tsx | 56 + client/src/components/ui/alert-dialog.tsx | 139 + client/src/components/ui/alert.tsx | 59 + client/src/components/ui/aspect-ratio.tsx | 5 + client/src/components/ui/avatar.tsx | 51 + client/src/components/ui/badge.tsx | 38 + client/src/components/ui/breadcrumb.tsx | 115 + client/src/components/ui/button.tsx | 62 + client/src/components/ui/calendar.tsx | 68 + client/src/components/ui/card.tsx | 85 + client/src/components/ui/carousel.tsx | 260 + client/src/components/ui/chart.tsx | 365 + client/src/components/ui/checkbox.tsx | 28 + client/src/components/ui/collapsible.tsx | 11 + client/src/components/ui/command.tsx | 151 + client/src/components/ui/context-menu.tsx | 198 + client/src/components/ui/dialog.tsx | 122 + client/src/components/ui/drawer.tsx | 118 + client/src/components/ui/dropdown-menu.tsx | 198 + client/src/components/ui/form.tsx | 178 + client/src/components/ui/hover-card.tsx | 29 + client/src/components/ui/input-otp.tsx | 69 + client/src/components/ui/input.tsx | 23 + client/src/components/ui/label.tsx | 24 + client/src/components/ui/menubar.tsx | 256 + client/src/components/ui/navigation-menu.tsx | 128 + client/src/components/ui/pagination.tsx | 117 + client/src/components/ui/popover.tsx | 29 + client/src/components/ui/progress.tsx | 28 + client/src/components/ui/radio-group.tsx | 42 + client/src/components/ui/resizable.tsx | 45 + client/src/components/ui/scroll-area.tsx | 46 + client/src/components/ui/select.tsx | 160 + client/src/components/ui/separator.tsx | 29 + client/src/components/ui/sheet.tsx | 140 + client/src/components/ui/sidebar.tsx | 727 ++ client/src/components/ui/skeleton.tsx | 15 + client/src/components/ui/slider.tsx | 26 + client/src/components/ui/switch.tsx | 27 + client/src/components/ui/table.tsx | 117 + client/src/components/ui/tabs.tsx | 53 + client/src/components/ui/textarea.tsx | 22 + client/src/components/ui/toast.tsx | 127 + client/src/components/ui/toaster.tsx | 33 + client/src/components/ui/toggle-group.tsx | 61 + client/src/components/ui/toggle.tsx | 43 + client/src/components/ui/tooltip.tsx | 30 + client/src/hooks/use-mobile.tsx | 19 + client/src/hooks/use-toast.ts | 191 + client/src/index.css | 328 + client/src/lib/queryClient.ts | 57 + client/src/lib/utils.ts | 6 + client/src/main.tsx | 5 + client/src/pages/not-found.tsx | 21 + components.json | 20 + design_guidelines.md | 79 + drizzle.config.ts | 14 + package-lock.json | 9116 +++++++++++++++++ package.json | 105 + postcss.config.js | 6 + server/index.ts | 71 + server/routes.ts | 15 + server/storage.ts | 38 + server/vite.ts | 85 + shared/schema.ts | 18 + tailwind.config.ts | 107 + tsconfig.json | 23 + vite.config.ts | 37 + 88 files changed, 17059 insertions(+) create mode 100644 .gitignore create mode 100644 .replit create mode 100644 client/index.html create mode 100644 client/src/App.tsx create mode 100644 client/src/components/BottomNavigation.tsx create mode 100644 client/src/components/CalendarView.tsx create mode 100644 client/src/components/KanbanBoard.tsx create mode 100644 client/src/components/ProjectTemplate.tsx create mode 100644 client/src/components/TaskCard.tsx create mode 100644 client/src/components/TaskCreationModal.tsx create mode 100644 client/src/components/TaskList.tsx create mode 100644 client/src/components/ThemeToggle.tsx create mode 100644 client/src/components/examples/BottomNavigation.tsx create mode 100644 client/src/components/examples/CalendarView.tsx create mode 100644 client/src/components/examples/KanbanBoard.tsx create mode 100644 client/src/components/examples/ProjectTemplate.tsx create mode 100644 client/src/components/examples/TaskCard.tsx create mode 100644 client/src/components/examples/TaskCreationModal.tsx create mode 100644 client/src/components/examples/TaskList.tsx create mode 100644 client/src/components/examples/ThemeToggle.tsx create mode 100644 client/src/components/ui/accordion.tsx create mode 100644 client/src/components/ui/alert-dialog.tsx create mode 100644 client/src/components/ui/alert.tsx create mode 100644 client/src/components/ui/aspect-ratio.tsx create mode 100644 client/src/components/ui/avatar.tsx create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/breadcrumb.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/calendar.tsx create mode 100644 client/src/components/ui/card.tsx create mode 100644 client/src/components/ui/carousel.tsx create mode 100644 client/src/components/ui/chart.tsx create mode 100644 client/src/components/ui/checkbox.tsx create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/command.tsx create mode 100644 client/src/components/ui/context-menu.tsx create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/components/ui/drawer.tsx create mode 100644 client/src/components/ui/dropdown-menu.tsx create mode 100644 client/src/components/ui/form.tsx create mode 100644 client/src/components/ui/hover-card.tsx create mode 100644 client/src/components/ui/input-otp.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/menubar.tsx create mode 100644 client/src/components/ui/navigation-menu.tsx create mode 100644 client/src/components/ui/pagination.tsx create mode 100644 client/src/components/ui/popover.tsx create mode 100644 client/src/components/ui/progress.tsx create mode 100644 client/src/components/ui/radio-group.tsx create mode 100644 client/src/components/ui/resizable.tsx create mode 100644 client/src/components/ui/scroll-area.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/components/ui/sheet.tsx create mode 100644 client/src/components/ui/sidebar.tsx create mode 100644 client/src/components/ui/skeleton.tsx create mode 100644 client/src/components/ui/slider.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/components/ui/table.tsx create mode 100644 client/src/components/ui/tabs.tsx create mode 100644 client/src/components/ui/textarea.tsx create mode 100644 client/src/components/ui/toast.tsx create mode 100644 client/src/components/ui/toaster.tsx create mode 100644 client/src/components/ui/toggle-group.tsx create mode 100644 client/src/components/ui/toggle.tsx create mode 100644 client/src/components/ui/tooltip.tsx create mode 100644 client/src/hooks/use-mobile.tsx create mode 100644 client/src/hooks/use-toast.ts create mode 100644 client/src/index.css create mode 100644 client/src/lib/queryClient.ts create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/not-found.tsx create mode 100644 components.json create mode 100644 design_guidelines.md create mode 100644 drizzle.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 server/index.ts create mode 100644 server/routes.ts create mode 100644 server/storage.ts create mode 100644 server/vite.ts create mode 100644 shared/schema.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts 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 + /> +
+ +
+