diff --git a/CLAUDE.md b/CLAUDE.md index 783a84f..641a901 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Claude Code Task Management Guide +# Codespace Task Management Guide ## Documentation Available diff --git a/app/dashboard/data.json b/app/dashboard/data.json index ec08736..80e9a7e 100644 --- a/app/dashboard/data.json +++ b/app/dashboard/data.json @@ -1,4 +1,5 @@ -[ +{ + "documents": [ { "id": 1, "header": "Cover page", @@ -611,4 +612,122 @@ "limit": "29", "reviewer": "Assign reviewer" } -] + ], + "kanban": { + "boards": [ + { + "id": "main-board", + "title": "Project Management Board", + "description": "Main kanban board for tracking project tasks", + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-15T10:30:00Z", + "columns": [ + { + "id": "todo-column", + "title": "To Do", + "status": "todo", + "maxTasks": 10, + "tasks": [ + { + "id": "task-1", + "title": "Design user authentication flow", + "description": "Create wireframes and user flow for the authentication system", + "status": "todo", + "priority": "high", + "assignee": "Sarah Johnson", + "createdAt": "2024-01-01T09:00:00Z", + "updatedAt": "2024-01-01T09:00:00Z", + "dueDate": "2024-01-20T17:00:00Z" + }, + { + "id": "task-2", + "title": "Set up development environment", + "description": "Configure Docker containers and development tools", + "status": "todo", + "priority": "medium", + "assignee": "Mike Chen", + "createdAt": "2024-01-02T10:00:00Z", + "updatedAt": "2024-01-02T10:00:00Z" + }, + { + "id": "task-3", + "title": "Research accessibility requirements", + "description": "Investigate WCAG guidelines and compliance needs", + "status": "todo", + "priority": "medium", + "createdAt": "2024-01-03T11:00:00Z", + "updatedAt": "2024-01-03T11:00:00Z" + } + ] + }, + { + "id": "inprogress-column", + "title": "In Progress", + "status": "in-progress", + "maxTasks": 5, + "tasks": [ + { + "id": "task-4", + "title": "Implement user registration API", + "description": "Build REST endpoints for user registration with validation", + "status": "in-progress", + "priority": "high", + "assignee": "Alex Rodriguez", + "createdAt": "2024-01-04T08:00:00Z", + "updatedAt": "2024-01-10T14:30:00Z", + "dueDate": "2024-01-18T17:00:00Z" + }, + { + "id": "task-5", + "title": "Create dashboard mockups", + "description": "Design main dashboard layout and components", + "status": "in-progress", + "priority": "medium", + "assignee": "Emma Davis", + "createdAt": "2024-01-05T09:30:00Z", + "updatedAt": "2024-01-12T16:45:00Z" + } + ] + }, + { + "id": "done-column", + "title": "Done", + "status": "done", + "tasks": [ + { + "id": "task-6", + "title": "Project planning and requirements", + "description": "Gather and document all project requirements", + "status": "done", + "priority": "high", + "assignee": "Lisa Wong", + "createdAt": "2023-12-20T10:00:00Z", + "updatedAt": "2024-01-05T15:00:00Z" + }, + { + "id": "task-7", + "title": "Database schema design", + "description": "Design and implement the database schema", + "status": "done", + "priority": "high", + "assignee": "David Kim", + "createdAt": "2023-12-22T11:00:00Z", + "updatedAt": "2024-01-08T12:30:00Z" + }, + { + "id": "task-8", + "title": "Set up CI/CD pipeline", + "description": "Configure automated testing and deployment", + "status": "done", + "priority": "medium", + "assignee": "James Wilson", + "createdAt": "2023-12-25T14:00:00Z", + "updatedAt": "2024-01-07T09:15:00Z" + } + ] + } + ] + } + ] + } +} diff --git a/app/dashboard/kanban/kanban.css b/app/dashboard/kanban/kanban.css new file mode 100644 index 0000000..ea5072d --- /dev/null +++ b/app/dashboard/kanban/kanban.css @@ -0,0 +1,95 @@ +/* Import kanban component styles */ +@import '../../../components/kanban/kanban.css'; + +/* Kanban Page Specific Styles */ +.kanban-page { + @apply min-h-screen bg-background; + overflow: hidden; +} + +.kanban-page .kanban-board { + height: calc(100vh - var(--header-height, 64px)); + padding: 1rem; +} + +/* Override scrollbar for kanban page */ +.kanban-page .kanban-scroll { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted-foreground)) hsl(var(--background)); +} + +.kanban-page .kanban-scroll::-webkit-scrollbar { + height: 6px; +} + +.kanban-page .kanban-scroll::-webkit-scrollbar-track { + background: hsl(var(--background)); + border-radius: 3px; +} + +.kanban-page .kanban-scroll::-webkit-scrollbar-thumb { + background: hsl(var(--muted)); + border-radius: 3px; +} + +.kanban-page .kanban-scroll::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground)); +} + +/* Loading state */ +.kanban-loading { + @apply flex items-center justify-center h-full; +} + +.kanban-loading-spinner { + @apply animate-spin rounded-full h-8 w-8 border-b-2 border-primary; +} + +/* Error state */ +.kanban-error { + @apply flex items-center justify-center h-full text-center; +} + +.kanban-error-icon { + @apply w-8 h-8 text-red-600 mx-auto mb-2; +} + +/* Responsive adjustments for kanban page */ +@media (max-width: 768px) { + .kanban-page .kanban-board { + padding: 0.5rem; + height: calc(100vh - var(--header-height, 64px) - 1rem); + } + + .kanban-page .kanban-columns { + gap: 1rem; + padding: 0.5rem; + } + + .kanban-page .kanban-column { + min-width: 280px; + max-width: 280px; + } +} + +@media (max-width: 640px) { + .kanban-page .kanban-board { + height: calc(100vh - var(--header-height, 56px) - 0.5rem); + } + + .kanban-page .kanban-column { + min-width: 260px; + max-width: 260px; + } +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .kanban-page { + background-color: hsl(var(--background)); + } + + .kanban-page .task-card:hover { + box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1); + } +} \ No newline at end of file diff --git a/app/dashboard/kanban/page.tsx b/app/dashboard/kanban/page.tsx new file mode 100644 index 0000000..62f6edf --- /dev/null +++ b/app/dashboard/kanban/page.tsx @@ -0,0 +1,95 @@ +'use client' + +import * as React from 'react' +import { KanbanBoard } from '@/components/kanban' +import { KanbanData, KanbanBoard as KanbanBoardType } from '@/types/kanban' +import data from '../data.json' +import './kanban.css' + +// This would normally come from metadata API route, but we'll set it here for now +const pageMetadata = { + title: 'Kanban Board - CodeGuide', + description: 'Manage your tasks and projects with our interactive Kanban board' +} + +export default function KanbanPage() { + const [boardData, setBoardData] = React.useState(null) + const [isLoading, setIsLoading] = React.useState(true) + const [error, setError] = React.useState(null) + + React.useEffect(() => { + // Set document title + document.title = pageMetadata.title + + try { + const kanbanData = data.kanban as KanbanData + if (kanbanData.boards && kanbanData.boards.length > 0) { + setBoardData(kanbanData.boards[0]) + } else { + setError('No kanban boards found') + } + } catch (err) { + setError('Failed to load kanban data') + console.error('Error loading kanban data:', err) + } finally { + setIsLoading(false) + } + }, []) + + const handleBoardChange = React.useCallback((updatedBoard: KanbanBoardType) => { + setBoardData(updatedBoard) + // In a real app, you might want to save to an API here + console.log('Board updated:', updatedBoard) + }, []) + + if (isLoading) { + return ( +
+
+
+

Loading kanban board...

+
+
+ ) + } + + if (error || !boardData) { + return ( +
+
+
+ + + +
+
+

Failed to load kanban board

+

+ {error || 'An unexpected error occurred'} +

+
+
+
+ ) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 42d379d..7c119d5 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -19,6 +19,7 @@ import { IconSearch, IconSettings, IconUsers, + IconLayoutKanban, } from "@tabler/icons-react" import { NavDocuments } from "@/components/nav-documents" @@ -39,9 +40,14 @@ const staticData = { navMain: [ { title: "Dashboard", - url: "#", + url: "/dashboard", icon: IconDashboard, }, + { + title: "Kanban Board", + url: "/dashboard/kanban", + icon: IconLayoutKanban, + }, { title: "Lifecycle", url: "#", diff --git a/components/kanban/KanbanBoard.tsx b/components/kanban/KanbanBoard.tsx new file mode 100644 index 0000000..ed4e53f --- /dev/null +++ b/components/kanban/KanbanBoard.tsx @@ -0,0 +1,194 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Plus, MoreHorizontal } from 'lucide-react' +import { cn } from '@/lib/utils' +import { KanbanBoard as KanbanBoardType, KanbanTask } from '@/types/kanban' +import { KanbanColumn } from './KanbanColumn' +import { TaskModal } from './TaskModal' +import { useKanbanState } from '@/hooks/useKanbanState' + +interface KanbanBoardProps { + initialBoard: KanbanBoardType + onBoardChange?: (board: KanbanBoardType) => void + className?: string +} + +export function KanbanBoard({ + initialBoard, + onBoardChange, + className +}: KanbanBoardProps) { + const { + board, + moveTask, + deleteTask, + saveTaskChanges, + getTask, + getColumn + } = useKanbanState(initialBoard) + + const [draggedTaskId, setDraggedTaskId] = React.useState(null) + const [taskModal, setTaskModal] = React.useState<{ + isOpen: boolean + mode: 'create' | 'edit' + task?: KanbanTask | null + columnId?: string + }>({ + isOpen: false, + mode: 'create', + task: null, + columnId: undefined + }) + + // Notify parent component when board changes + React.useEffect(() => { + onBoardChange?.(board) + }, [board, onBoardChange]) + + const handleDragStart = (taskId: string) => { + setDraggedTaskId(taskId) + } + + const handleDragEnd = () => { + setDraggedTaskId(null) + } + + const handleTaskDrop = (taskId: string, newColumnId: string) => { + const sourceColumn = board.columns.find(col => + col.tasks.some(task => task.id === taskId) + ) + + const targetColumn = board.columns.find(col => col.id === newColumnId) + + if (!sourceColumn || !targetColumn || sourceColumn.id === newColumnId) { + return + } + + moveTask(taskId, newColumnId, targetColumn.status) + } + + const handleTaskEdit = (task: KanbanTask) => { + setTaskModal({ + isOpen: true, + mode: 'edit', + task, + columnId: undefined + }) + } + + const handleTaskDelete = (taskId: string) => { + if (window.confirm('Are you sure you want to delete this task?')) { + deleteTask(taskId) + } + } + + const handleTaskAdd = (columnId: string) => { + const column = getColumn(columnId) + setTaskModal({ + isOpen: true, + mode: 'create', + task: null, + columnId + }) + } + + const handleModalClose = () => { + setTaskModal({ + isOpen: false, + mode: 'create', + task: null, + columnId: undefined + }) + } + + const handleTaskSave = (taskData: Partial) => { + saveTaskChanges(taskData, taskModal.columnId) + } + + return ( +
+ {/* Board Header */} + + +
+
+ + {board.title} + + {board.description && ( +

+ {board.description} +

+ )} +
+ +
+ + +
+
+
+
+ + {/* Kanban Columns */} +
+
+
+ {board.columns.map((column) => ( + + ))} + + {/* Add Column Button */} +
+ + + + + +
+
+
+
+ + {/* Task Modal */} + +
+ ) +} \ No newline at end of file diff --git a/components/kanban/KanbanColumn.tsx b/components/kanban/KanbanColumn.tsx new file mode 100644 index 0000000..e8fd761 --- /dev/null +++ b/components/kanban/KanbanColumn.tsx @@ -0,0 +1,167 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Plus } from 'lucide-react' +import { cn } from '@/lib/utils' +import { KanbanColumn as KanbanColumnType, KanbanTask } from '@/types/kanban' +import { TaskCard } from './TaskCard' + +interface KanbanColumnProps { + column: KanbanColumnType + onTaskEdit?: (task: KanbanTask) => void + onTaskDelete?: (taskId: string) => void + onTaskAdd?: (columnId: string) => void + onTaskDrop?: (taskId: string, newColumnId: string) => void + onDragStart?: (taskId: string) => void + onDragEnd?: () => void + isDragOver?: boolean + draggedTaskId?: string | null +} + +export function KanbanColumn({ + column, + onTaskEdit, + onTaskDelete, + onTaskAdd, + onTaskDrop, + onDragStart, + onDragEnd, + isDragOver = false, + draggedTaskId +}: KanbanColumnProps) { + const [draggedOver, setDraggedOver] = React.useState(false) + const [dragDepth, setDragDepth] = React.useState(0) + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + + // Only show drag over state if the dragged task isn't already in this column + const taskId = e.dataTransfer.getData('text/plain') || draggedTaskId + const taskExistsInColumn = column.tasks.some(task => task.id === taskId) + + if (!taskExistsInColumn) { + setDraggedOver(true) + } + } + + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault() + setDragDepth(prev => prev + 1) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setDragDepth(prev => { + const newDepth = prev - 1 + if (newDepth <= 0) { + setDraggedOver(false) + } + return Math.max(0, newDepth) + }) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDraggedOver(false) + setDragDepth(0) + + const taskId = e.dataTransfer.getData('text/plain') + if (taskId && onTaskDrop) { + // Check if task is not already in this column + const taskExistsInColumn = column.tasks.some(task => task.id === taskId) + if (!taskExistsInColumn) { + onTaskDrop(taskId, column.id) + } + } + } + + const getColumnColor = (status: string) => { + switch (status) { + case 'todo': + return 'bg-slate-100 text-slate-800' + case 'in-progress': + return 'bg-blue-100 text-blue-800' + case 'done': + return 'bg-green-100 text-green-800' + default: + return 'bg-gray-100 text-gray-800' + } + } + + return ( +
+ + +
+
+ + {column.title} + + + {column.tasks.length} + {column.maxTasks && ` / ${column.maxTasks}`} + +
+ + +
+
+ + + {column.tasks.length === 0 && ( +
+ No tasks +
+ )} + + {column.tasks.map((task) => ( +
+ +
+ ))} + + {draggedOver && column.tasks.length === 0 && ( +
+ + Drop task here +
+ )} + + {draggedOver && column.tasks.length > 0 && ( +
+ )} + + +
+ ) +} \ No newline at end of file diff --git a/components/kanban/TaskCard.tsx b/components/kanban/TaskCard.tsx new file mode 100644 index 0000000..9a6c178 --- /dev/null +++ b/components/kanban/TaskCard.tsx @@ -0,0 +1,155 @@ +'use client' + +import * as React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu' +import { MoreVertical, Edit2, Trash2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import { KanbanTask } from '@/types/kanban' + +interface TaskCardProps { + task: KanbanTask + onEdit?: (task: KanbanTask) => void + onDelete?: (taskId: string) => void + isDragging?: boolean + onDragStart?: (taskId: string) => void + onDragEnd?: () => void +} + +export function TaskCard({ + task, + onEdit, + onDelete, + isDragging = false, + onDragStart, + onDragEnd +}: TaskCardProps) { + const [dragState, setDragState] = React.useState<'idle' | 'dragging' | 'drag-over'>('idle') + const priorityColors = { + low: 'bg-green-100 text-green-800 hover:bg-green-200', + medium: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200', + high: 'bg-red-100 text-red-800 hover:bg-red-200' + } + + const getInitials = (name: string) => { + return name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }) + } + + const handleDragStart = (e: React.DragEvent) => { + e.dataTransfer.setData('text/plain', task.id) + e.dataTransfer.effectAllowed = 'move' + setDragState('dragging') + onDragStart?.(task.id) + + // Create a custom drag image + const dragImage = e.currentTarget.cloneNode(true) as HTMLElement + dragImage.style.transform = 'rotate(3deg)' + dragImage.style.opacity = '0.8' + document.body.appendChild(dragImage) + e.dataTransfer.setDragImage(dragImage, 50, 50) + + setTimeout(() => { + document.body.removeChild(dragImage) + }, 0) + } + + const handleDragEnd = (e: React.DragEvent) => { + setDragState('idle') + onDragEnd?.() + } + + return ( + + +
+ + {task.title} + + + + + + + onEdit?.(task)}> + + Edit + + + onDelete?.(task.id)} + className="text-red-600 hover:text-red-700" + > + + Delete + + + +
+
+ + + {task.description && ( +

+ {task.description} +

+ )} + +
+
+ + {task.priority} + + + {task.dueDate && ( + + {formatDate(task.dueDate)} + + )} +
+ + {task.assignee && ( + + + + {getInitials(task.assignee)} + + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/components/kanban/TaskModal.tsx b/components/kanban/TaskModal.tsx new file mode 100644 index 0000000..232860f --- /dev/null +++ b/components/kanban/TaskModal.tsx @@ -0,0 +1,255 @@ +'use client' + +import * as React from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { KanbanTask } from '@/types/kanban' +import { Calendar, CalendarDays, X } from 'lucide-react' + +interface TaskModalProps { + isOpen: boolean + onClose: () => void + onSave: (task: Partial) => void + task?: KanbanTask | null + mode: 'create' | 'edit' + columnId?: string + columnStatus?: 'todo' | 'in-progress' | 'done' +} + +export function TaskModal({ + isOpen, + onClose, + onSave, + task, + mode, + columnId, + columnStatus +}: TaskModalProps) { + const [formData, setFormData] = React.useState({ + title: '', + description: '', + priority: 'medium' as 'low' | 'medium' | 'high', + assignee: '', + dueDate: '' + }) + + const [errors, setErrors] = React.useState>({}) + const [isLoading, setIsLoading] = React.useState(false) + + // Initialize form data when task or modal opens + React.useEffect(() => { + if (isOpen) { + if (mode === 'edit' && task) { + setFormData({ + title: task.title, + description: task.description, + priority: task.priority, + assignee: task.assignee || '', + dueDate: task.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : '' + }) + } else { + // Reset form for new task + setFormData({ + title: '', + description: '', + priority: 'medium', + assignee: '', + dueDate: '' + }) + } + setErrors({}) + } + }, [isOpen, task, mode]) + + const validateForm = () => { + const newErrors: Record = {} + + if (!formData.title.trim()) { + newErrors.title = 'Title is required' + } + + if (formData.title.length > 100) { + newErrors.title = 'Title must be less than 100 characters' + } + + if (formData.description.length > 500) { + newErrors.description = 'Description must be less than 500 characters' + } + + if (formData.assignee.length > 50) { + newErrors.assignee = 'Assignee name must be less than 50 characters' + } + + if (formData.dueDate) { + const dueDate = new Date(formData.dueDate) + const today = new Date() + today.setHours(0, 0, 0, 0) + + if (dueDate < today) { + newErrors.dueDate = 'Due date cannot be in the past' + } + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validateForm()) { + return + } + + setIsLoading(true) + + try { + const taskData: Partial = { + ...formData, + dueDate: formData.dueDate ? new Date(formData.dueDate).toISOString() : undefined, + updatedAt: new Date().toISOString() + } + + if (mode === 'create') { + taskData.id = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + taskData.status = columnStatus || 'todo' + taskData.createdAt = new Date().toISOString() + } else if (task) { + taskData.id = task.id + taskData.status = task.status + taskData.createdAt = task.createdAt + } + + await onSave(taskData) + onClose() + } catch (error) { + console.error('Error saving task:', error) + } finally { + setIsLoading(false) + } + } + + const handleInputChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })) + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })) + } + } + + return ( + + + +
+ + {mode === 'create' ? 'Create New Task' : 'Edit Task'} + + +
+
+ +
+
+ + handleInputChange('title', e.target.value)} + placeholder="Enter task title" + className={errors.title ? 'border-red-500' : ''} + /> + {errors.title && ( +

{errors.title}

+ )} +
+ +
+ +