"use client" import { useState, useEffect, useMemo, type ChangeEvent, type ReactNode, Suspense } from "react" import dynamic from "next/dynamic" import { useDebounce } from "@/hooks/useDebounce" import { DndContext, DragEndEvent, DragOverlay, DragOverEvent, DragStartEvent, PointerSensor, useDraggable, useDroppable, useSensor, useSensors, closestCorners, } from "@dnd-kit/core" import { CSS } from "@dnd-kit/utilities" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" import { isSprintInProgress, parseSprintEnd, parseSprintStart } from "@/lib/utils" import { generateAvatarDataUrl } from "@/lib/avatar" import { blobFromDataUrl, coerceDataUrlMimeType, inferAttachmentMimeType, isMarkdownAttachment, isTextPreviewAttachment, markdownPreviewObjectUrl, textPreviewObjectUrl, } from "@/lib/attachments" import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment } from "@/stores/useTaskStore" import { BacklogSkeleton, SearchSkeleton } from "@/components/LoadingSkeletons" import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search, Archive } from "lucide-react" import { toast } from "sonner" // Dynamic imports for heavy view components - only load when needed const BacklogView = dynamic(() => import("@/components/BacklogView").then(mod => mod.BacklogView), { loading: () => , ssr: false, }) const SearchView = dynamic(() => import("@/components/SearchView").then(mod => mod.SearchView), { loading: () => , ssr: false, }) interface AssignableUser { id: string name: string email?: string avatarUrl?: string } const typeColors: Record = { idea: "bg-purple-500", task: "bg-blue-500", bug: "bg-red-500", research: "bg-green-500", plan: "bg-amber-500", } const typeLabels: Record = { idea: "๐Ÿ’ก Idea", task: "๐Ÿ“‹ Task", bug: "๐Ÿ› Bug", research: "๐Ÿ”ฌ Research", plan: "๐Ÿ“ Plan", } const priorityColors: Record = { low: "text-slate-400", medium: "text-blue-400", high: "text-orange-400", urgent: "text-red-400", } const allStatuses: TaskStatus[] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"] function AvatarCircle({ name, avatarUrl, seed, sizeClass = "h-6 w-6", title, }: { name?: string avatarUrl?: string seed?: string sizeClass?: string title?: string }) { const displayUrl = avatarUrl || generateAvatarDataUrl(seed || name || "default", name || "User") return ( {name ) } // Sprint board columns mapped to workflow statuses const sprintColumns: { key: string; label: string; statuses: TaskStatus[] }[] = [ { key: "todo", label: "To Do", statuses: ["open", "todo"] }, { key: "inprogress", label: "In Progress", statuses: ["blocked", "in-progress", "review", "validate"] }, { key: "done", label: "Done", statuses: ["archived", "canceled", "done"] }, ] const sprintColumnDropStatus: Record = { todo: "open", inprogress: "in-progress", done: "done", } const formatStatusLabel = (status: TaskStatus) => status === "todo" ? "To Do" : status .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" ") const formatSprintDisplayDate = (value: string, boundary: "start" | "end" = "start") => { const parsed = boundary === "end" ? parseSprintEnd(value) : parseSprintStart(value) return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString() } function KanbanStatusDropTarget({ status, count, expanded = false, }: { status: TaskStatus count: number expanded?: boolean }) { const { isOver, setNodeRef } = useDroppable({ id: `kanban-status-${status}` }) return (
{formatStatusLabel(status)} ({count})
) } function KanbanDropColumn({ id, label, count, statusTargets, isDragging, isActiveDropColumn, children, }: { id: string label: string count: number statusTargets: Array<{ status: TaskStatus; count: number }> isDragging: boolean isActiveDropColumn: boolean children: ReactNode }) { const { isOver, setNodeRef } = useDroppable({ id }) return (

{label}

{count}
{statusTargets.map((target) => ( ))}
{isDragging && isActiveDropColumn ? (

Drop into an exact status:

{statusTargets.map((target) => ( ))}
) : ( children )}
) } function KanbanTaskCard({ task, taskTags, assigneeAvatarUrl, onOpen, onDelete, }: { task: Task taskTags: string[] assigneeAvatarUrl?: string onOpen: () => void onDelete: () => void }) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id, }) const style = { transform: CSS.Translate.toString(transform), opacity: isDragging ? 0.6 : 1, } const attachmentCount = task.attachments?.length || 0 return (
{typeLabels[task.type]}

{task.title}

{task.description && (

{task.description}

)}
{formatStatusLabel(task.status)} {task.priority} {task.comments && task.comments.length > 0 && ( {task.comments.length} )} {attachmentCount > 0 && ( {attachmentCount} )}
{task.dueDate && ( {new Date(task.dueDate).toLocaleDateString()} )}
{taskTags.length > 0 && (
{taskTags.map((tag) => ( {tag} ))}
)}
) } export default function Home() { console.log('>>> PAGE: Component rendering') const router = useRouter() const { projects, tasks, sprints, currentUser, selectedProjectId, selectedTaskId, addTask, updateTask, deleteTask, selectTask, addComment, deleteComment, setCurrentUser, syncFromServer, isLoading, updateSprint, } = useTaskStore() const [newTaskOpen, setNewTaskOpen] = useState(false) const [newTask, setNewTask] = useState>({ title: "", description: "", type: "task", priority: "medium", status: "open", tags: [], }) const [newComment, setNewComment] = useState("") const [viewMode, setViewMode] = useState<'kanban' | 'backlog' | 'search'>('kanban') const [editedTask, setEditedTask] = useState(null) const [newTaskLabelInput, setNewTaskLabelInput] = useState("") const [editedTaskLabelInput, setEditedTaskLabelInput] = useState("") const [activeKanbanTaskId, setActiveKanbanTaskId] = useState(null) const [dragOverKanbanColumnKey, setDragOverKanbanColumnKey] = useState(null) const [authReady, setAuthReady] = useState(false) const [initialSyncComplete, setInitialSyncComplete] = useState(false) const [users, setUsers] = useState([]) const [searchQuery, setSearchQuery] = useState("") const debouncedSearchQuery = useDebounce(searchQuery, 300) const getTags = (taskLike: { tags?: unknown }) => { if (!Array.isArray(taskLike.tags)) return [] as string[] return taskLike.tags.filter((tag): tag is string => typeof tag === "string" && tag.trim().length > 0) } const getAttachments = (taskLike: { attachments?: unknown }) => { if (!Array.isArray(taskLike.attachments)) return [] as TaskAttachment[] return taskLike.attachments.filter((attachment): attachment is TaskAttachment => { if (!attachment || typeof attachment !== "object") return false const candidate = attachment as Partial return typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.dataUrl === "string" && typeof candidate.uploadedAt === "string" && typeof candidate.type === "string" && typeof candidate.size === "number" }) } const getCommentAuthorId = (value: unknown): string | null => typeof value === "string" && value.trim().length > 0 ? value.trim() : null const formatBytes = (bytes: number) => { if (!Number.isFinite(bytes) || bytes <= 0) return "0 B" const units = ["B", "KB", "MB", "GB"] const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1) const value = bytes / 1024 ** unitIndex return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}` } const readFileAsDataUrl = (file: File) => new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(String(reader.result || "")) reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`)) reader.readAsDataURL(file) }) const labelUsage = useMemo(() => { const counts = new Map() tasks.forEach((task) => { getTags(task).forEach((label) => { counts.set(label, (counts.get(label) || 0) + 1) }) }) return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]) }, [tasks]) const allLabels = useMemo(() => labelUsage.map(([label]) => label), [labelUsage]) const assignableUsers = useMemo(() => { const byId = new Map() users.forEach((user) => { if (user.id) byId.set(user.id, user) }) if (currentUser.id) { byId.set(currentUser.id, { id: currentUser.id, name: currentUser.name, email: currentUser.email, avatarUrl: currentUser.avatarUrl, }) } return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name)) }, [users, currentUser]) const knownUsersById = useMemo(() => { const byId = new Map() const addUser = (id: string | undefined, name: string | undefined, avatarUrl?: string, email?: string) => { if (!id || !name) return if (id.trim().length === 0 || name.trim().length === 0) return byId.set(id, { id, name, avatarUrl, email }) } assignableUsers.forEach((user) => byId.set(user.id, user)) tasks.forEach((task) => { addUser(task.createdById, task.createdByName, task.createdByAvatarUrl) addUser(task.updatedById, task.updatedByName, task.updatedByAvatarUrl) addUser(task.assigneeId, task.assigneeName, task.assigneeAvatarUrl, task.assigneeEmail) }) return byId }, [assignableUsers, tasks]) useEffect(() => { let isMounted = true const loadSession = async () => { try { const res = await fetch("/api/auth/session", { cache: "no-store" }) if (!res.ok) { if (isMounted) router.replace("/login") return } const data = await res.json() if (!isMounted) return setCurrentUser({ id: data.user.id, name: data.user.name, email: data.user.email, avatarUrl: data.user.avatarUrl, }) setAuthReady(true) } catch { if (isMounted) router.replace("/login") } } loadSession() return () => { isMounted = false } }, [router, setCurrentUser]) useEffect(() => { if (!authReady) return let active = true setInitialSyncComplete(false) const runInitialSync = async () => { await syncFromServer() if (active) setInitialSyncComplete(true) } void runInitialSync() return () => { active = false } }, [authReady, syncFromServer]) useEffect(() => { if (!authReady) return let isMounted = true const loadUsers = async () => { try { const res = await fetch("/api/auth/users", { cache: "no-store" }) if (!res.ok) return const data = await res.json() if (!isMounted) return const nextUsers = Array.isArray(data.users) ? (data.users as Array>) : [] setUsers( nextUsers .filter((entry): entry is Partial & { id: string; name: string } => !!entry && typeof entry.id === "string" && typeof entry.name === "string" ) .map((entry) => ({ id: entry.id, name: entry.name, email: entry.email, avatarUrl: entry.avatarUrl })) ) } catch { // ignore } } loadUsers() return () => { isMounted = false } }, [authReady]) useEffect(() => { if (!authReady) return setNewTask((prev) => { if (prev.assigneeId) return prev return { ...prev, assigneeId: currentUser.id, assigneeName: currentUser.name, assigneeEmail: currentUser.email, assigneeAvatarUrl: currentUser.avatarUrl, } }) }, [authReady, currentUser.id, currentUser.name, currentUser.email, currentUser.avatarUrl]) useEffect(() => { if (selectedTaskId) { selectTask(null) } }, [selectedTaskId, selectTask]) // Log when tasks change useEffect(() => { console.log('>>> PAGE: tasks changed, new count:', tasks.length) }, [tasks]) // Auto-switch to search view when user types in search box useEffect(() => { if (debouncedSearchQuery.trim() && viewMode !== 'search') { setViewMode('search') } }, [debouncedSearchQuery, viewMode]) const selectedTask = tasks.find((t) => t.id === selectedTaskId) const editedTaskTags = editedTask ? getTags(editedTask) : [] const editedTaskAttachments = editedTask ? getAttachments(editedTask) : [] useEffect(() => { if (selectedTask) { setEditedTask({ ...selectedTask, tags: getTags(selectedTask), attachments: getAttachments(selectedTask), }) setEditedTaskLabelInput("") } }, [selectedTask]) // Get current sprint (across all projects) using local-day boundaries. const now = new Date() const currentSprint = sprints.find((s) => s.status === "active" && isSprintInProgress(s.startDate, s.endDate, now)) ?? sprints.find((s) => s.status !== "completed" && isSprintInProgress(s.startDate, s.endDate, now)) // Filter tasks to only show current sprint tasks in Kanban (from ALL projects) // Sort by updatedAt descending (latest first) const sprintTasks = currentSprint ? tasks .filter((t) => { if (t.sprintId !== currentSprint.id) return false // Apply search filter if (debouncedSearchQuery.trim()) { const query = debouncedSearchQuery.toLowerCase() const matchesTitle = t.title.toLowerCase().includes(query) const matchesDescription = t.description?.toLowerCase().includes(query) ?? false return matchesTitle || matchesDescription } return true }) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) : [] // Auto-rollover: Move incomplete tasks from ended sprints to next sprint useEffect(() => { if (!authReady || sprints.length === 0) return const now = new Date() const endedSprints = sprints.filter((s) => { if (s.status === "completed") return false const sprintEnd = parseSprintEnd(s.endDate) return sprintEnd < now }) if (endedSprints.length === 0) return // Find next sprint (earliest start date that's in the future or active) const nextSprint = sprints .filter((s) => s.status !== "completed" && !endedSprints.find((e) => e.id === s.id)) .sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())[0] if (!nextSprint) return // Process each ended sprint endedSprints.forEach((endedSprint) => { const incompleteTasks = tasks.filter( (t) => t.sprintId === endedSprint.id && t.status !== 'done' && t.status !== 'canceled' && t.status !== 'archived' ) if (incompleteTasks.length > 0) { console.log(`Auto-rolling over ${incompleteTasks.length} tasks from "${endedSprint.name}" to "${nextSprint.name}"`) // Move incomplete tasks to next sprint incompleteTasks.forEach((task) => { updateTask(task.id, { sprintId: nextSprint.id }) }) // Mark ended sprint as completed updateSprint(endedSprint.id, { status: 'completed' }) } else { // No incomplete tasks, just mark as completed updateSprint(endedSprint.id, { status: 'completed' }) } }) }, [authReady, sprints, tasks, updateTask, updateSprint]) const activeKanbanTask = activeKanbanTaskId ? sprintTasks.find((task) => task.id === activeKanbanTaskId) : null const toLabel = (raw: string) => raw.trim().replace(/^#/, "") const addUniqueLabel = (existing: string[], raw: string) => { const nextLabel = toLabel(raw) if (!nextLabel) return existing const alreadyExists = existing.some((label) => label.toLowerCase() === nextLabel.toLowerCase()) return alreadyExists ? existing : [...existing, nextLabel] } const removeLabel = (existing: string[], labelToRemove: string) => existing.filter((label) => label.toLowerCase() !== labelToRemove.toLowerCase()) const getColumnKeyForStatus = (status: TaskStatus) => sprintColumns.find((column) => column.statuses.includes(status))?.key const kanbanSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ) const handleKanbanDragStart = (event: DragStartEvent) => { setActiveKanbanTaskId(String(event.active.id)) } const resolveKanbanColumnKey = (overId: string): string | null => { if (overId.startsWith("kanban-col-")) { return overId.replace("kanban-col-", "") } if (overId.startsWith("kanban-status-")) { const status = overId.replace("kanban-status-", "") as TaskStatus return getColumnKeyForStatus(status) || null } const overTask = sprintTasks.find((task) => task.id === overId) if (!overTask) return null return getColumnKeyForStatus(overTask.status) || null } const handleKanbanDragOver = (event: DragOverEvent) => { if (!event.over) { setDragOverKanbanColumnKey(null) return } setDragOverKanbanColumnKey(resolveKanbanColumnKey(String(event.over.id))) } const handleKanbanDragEnd = (event: DragEndEvent) => { const { active, over } = event setActiveKanbanTaskId(null) setDragOverKanbanColumnKey(null) if (!over) return const taskId = String(active.id) const draggedTask = sprintTasks.find((task) => task.id === taskId) if (!draggedTask) return const destination = String(over.id) const overTask = sprintTasks.find((task) => task.id === destination) if (overTask) { if (overTask.status !== draggedTask.status) { updateTask(taskId, { status: overTask.status }) } return } if (destination.startsWith("kanban-status-")) { const exactStatus = destination.replace("kanban-status-", "") as TaskStatus if (exactStatus !== draggedTask.status) { updateTask(taskId, { status: exactStatus }) } return } if (!destination.startsWith("kanban-col-")) return const destinationColumnKey = destination.replace("kanban-col-", "") const sourceColumnKey = getColumnKeyForStatus(draggedTask.status) if (sourceColumnKey === destinationColumnKey) return const newStatus = sprintColumnDropStatus[destinationColumnKey] if (!newStatus) return updateTask(taskId, { status: newStatus }) } const handleKanbanDragCancel = () => { setActiveKanbanTaskId(null) setDragOverKanbanColumnKey(null) } const resolveAssignee = (assigneeId: string | undefined) => { if (!assigneeId) return null return knownUsersById.get(assigneeId) || null } const setNewTaskAssignee = (assigneeId: string) => { if (!assigneeId) { setNewTask((prev) => ({ ...prev, assigneeId: undefined, assigneeName: undefined, assigneeEmail: undefined, assigneeAvatarUrl: undefined, })) return } const assignee = resolveAssignee(assigneeId) setNewTask((prev) => ({ ...prev, assigneeId, assigneeName: assignee?.name || prev.assigneeName, assigneeEmail: assignee?.email, assigneeAvatarUrl: assignee?.avatarUrl, })) } const setEditedTaskAssignee = (assigneeId: string) => { if (!editedTask) return if (!assigneeId) { setEditedTask({ ...editedTask, assigneeId: undefined, assigneeName: undefined, assigneeEmail: undefined, assigneeAvatarUrl: undefined, }) return } const assignee = resolveAssignee(assigneeId) setEditedTask({ ...editedTask, assigneeId, assigneeName: assignee?.name || editedTask.assigneeName, assigneeEmail: assignee?.email, assigneeAvatarUrl: assignee?.avatarUrl, }) } const handleAddTask = () => { if (newTask.title?.trim()) { // If a specific sprint is selected, use that sprint's project const selectedSprint = newTask.sprintId ? sprints.find(s => s.id === newTask.sprintId) : null const targetProjectId = selectedSprint?.projectId || selectedProjectId || projects[0]?.id if (!targetProjectId) { toast.error("Cannot create task", { description: "No project is available. Create or select a project first.", duration: 5000, }) return } const taskToCreate: Omit = { title: newTask.title.trim(), description: newTask.description?.trim() || undefined, type: (newTask.type || "task") as TaskType, priority: (newTask.priority || "medium") as Priority, status: (newTask.status || "open") as TaskStatus, tags: newTask.tags || [], projectId: targetProjectId, sprintId: newTask.sprintId || currentSprint?.id, assigneeId: newTask.assigneeId, assigneeName: newTask.assigneeName, assigneeEmail: newTask.assigneeEmail, assigneeAvatarUrl: newTask.assigneeAvatarUrl, } addTask(taskToCreate) setNewTask({ title: "", description: "", type: "task", priority: "medium", status: "open", tags: [], sprintId: undefined, assigneeId: currentUser.id, assigneeName: currentUser.name, assigneeEmail: currentUser.email, assigneeAvatarUrl: currentUser.avatarUrl, }) setNewTaskLabelInput("") setNewTaskOpen(false) } } const handleAddComment = () => { if (newComment.trim() && selectedTaskId) { const commentAuthorId = getCommentAuthorId(currentUser.id) if (!commentAuthorId) { toast.error("You must be signed in to add a comment.") return } addComment(selectedTaskId, newComment.trim(), commentAuthorId) setNewComment("") } } const handleLogout = async () => { try { await fetch("/api/auth/logout", { method: "POST" }) } finally { setAuthReady(false) router.replace("/login") } } const handleAttachmentUpload = async (event: ChangeEvent) => { const files = Array.from(event.target.files || []) if (files.length === 0) return try { const uploadedAt = new Date().toISOString() const attachments = await Promise.all( files.map(async (file) => { const type = inferAttachmentMimeType(file.name, file.type) const rawDataUrl = await readFileAsDataUrl(file) return { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name: file.name, type, size: file.size, dataUrl: coerceDataUrlMimeType(rawDataUrl, type), uploadedAt, } }) ) setEditedTask((prev) => { if (!prev) return prev return { ...prev, attachments: [...getAttachments(prev), ...attachments], } }) } catch (error) { console.error("Failed to upload attachment:", error) } finally { event.target.value = "" } } const openAttachment = async (attachment: TaskAttachment) => { try { const mimeType = inferAttachmentMimeType(attachment.name, attachment.type) const objectUrl = isMarkdownAttachment(attachment.name, mimeType) ? await markdownPreviewObjectUrl(attachment.name, attachment.dataUrl) : isTextPreviewAttachment(attachment.name, mimeType) ? await textPreviewObjectUrl(attachment.name, attachment.dataUrl, mimeType) : URL.createObjectURL(await blobFromDataUrl(attachment.dataUrl, mimeType)) window.open(objectUrl, "_blank", "noopener,noreferrer") window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000) } catch (error) { console.error("Failed to open attachment:", error) } } const downloadAttachment = async (attachment: TaskAttachment) => { try { const mimeType = inferAttachmentMimeType(attachment.name, attachment.type) const blob = await blobFromDataUrl(attachment.dataUrl, mimeType) const objectUrl = URL.createObjectURL(blob) const link = document.createElement("a") link.href = objectUrl link.download = attachment.name document.body.appendChild(link) link.click() link.remove() window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000) } catch (error) { console.error("Failed to download attachment:", error) } } if (!authReady) { return (

Checking session...

) } if (!initialSyncComplete) { return (

Loading board...

) } return (
{/* Header */}

OpenClaw Task Hub

Track ideas, tasks, bugs, and plans โ€” with threaded comments

{/* Search Input */}
setSearchQuery(e.target.value)} placeholder="Search tasks..." className="w-48 lg:w-64 pl-9 pr-8 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 transition-colors" /> {searchQuery && ( )}
{isLoading && ( Syncing... )} {tasks.length} tasks ยท {allLabels.length} labels
{currentUser.name}
{/* Main Content */}

{currentSprint ? currentSprint.name : "Work Board"}

{debouncedSearchQuery.trim() ? ( <> {sprintTasks.length} of {currentSprint ? tasks.filter((t) => t.sprintId === currentSprint.id).length : 0} tasks match "{debouncedSearchQuery}" ) : ( <> {sprintTasks.length} tasks ยท {sprintTasks.filter((t) => t.status === "done").length} done )}

{/* View Toggle */}
{/* Mobile Search - shown only on small screens */}
setSearchQuery(e.target.value)} placeholder="Search tasks..." className="w-full pl-9 pr-8 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" /> {searchQuery && ( )}
{/* View Content */} {viewMode === 'search' ? ( ) : viewMode === 'backlog' ? ( ) : ( <> {/* Current Sprint Header */}

{currentSprint?.name || "No Active Sprint"}

{currentSprint ? `${formatSprintDisplayDate(currentSprint.startDate, "start")} - ${formatSprintDisplayDate(currentSprint.endDate, "end")}` : "Create or activate a sprint to group work"}

{currentSprint && Active}
{/* Kanban Columns */}
{sprintColumns.map((column) => { const columnTasks = sprintTasks.filter((t) => column.statuses.includes(t.status) ) return ( ({ status, count: sprintTasks.filter((task) => task.status === status).length, }))} isDragging={!!activeKanbanTaskId} isActiveDropColumn={dragOverKanbanColumnKey === column.key} > {columnTasks.map((task) => ( router.push(`/tasks/${encodeURIComponent(task.id)}`)} onDelete={() => deleteTask(task.id)} /> ))} ) })}
{activeKanbanTask ? (
{activeKanbanTask.title}
) : null}
)}
{/* New Task Dialog */} New Task
setNewTask({ ...newTask, title: e.target.value })} className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" placeholder="What needs to be done?" />