From 1fd30d7d4df19d36e61244dafeafd2bc2a614310 Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Wed, 11 Feb 2026 23:02:41 -0300 Subject: [PATCH 01/13] refact: redesign BoardHeader with filter tabs and display button Replace search input and New Task button with title + issue counter, filter tabs (Projects, Teams, Date) and a Display button. --- .../web/src/components/board/board-header.tsx | 79 ++++++++++++------- apps/web/src/components/board/task-board.tsx | 7 +- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/apps/web/src/components/board/board-header.tsx b/apps/web/src/components/board/board-header.tsx index 2727605..b780b9c 100644 --- a/apps/web/src/components/board/board-header.tsx +++ b/apps/web/src/components/board/board-header.tsx @@ -1,50 +1,69 @@ "use client"; -import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; +import { useState } from "react"; +import { + Folder, + UsersThree, + CalendarBlank, + SlidersHorizontal, +} from "@phosphor-icons/react"; + +const FILTER_TABS = [ + { id: "projects", label: "Projects", icon: Folder }, + { id: "teams", label: "Teams", icon: UsersThree }, + { id: "date", label: "Date", icon: CalendarBlank }, +] as const; + +type FilterTab = (typeof FILTER_TABS)[number]["id"]; interface BoardHeaderProps { title: string; - subtitle: string; - searchQuery: string; - onSearchChange: (query: string) => void; - onNewTask?: () => void; + issueCount: number; } -export function BoardHeader({ - title, - subtitle, - searchQuery, - onSearchChange, - onNewTask, -}: BoardHeaderProps) { +export function BoardHeader({ title, issueCount }: BoardHeaderProps) { + const [activeTab, setActiveTab] = useState("projects"); + return ( -
-
-

+
+
+

{title}

-

{subtitle}

+ + {issueCount} Issues +
-
-
- - onSearchChange(e.target.value)} - placeholder="Search tasks..." - className="flex-1 bg-transparent text-foreground text-sm placeholder:text-muted-foreground focus:outline-none" - /> +
+
+ {FILTER_TABS.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })}
diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index 9a8ce5f..a1227ca 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -583,11 +583,8 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { return (
setIsModalOpen(true)} + title="Board" + issueCount={totalTasks} /> {localColumns.length === 0 ? ( From 0cfecc72b647f0604e3e91868b0d0c389790fc76 Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Wed, 11 Feb 2026 23:19:54 -0300 Subject: [PATCH 02/13] refact: redesign kanban column header with status icons and descriptions Add status icons per column, task counter, description line, and ghost add-task button. Remove colored border and background for a cleaner look. --- .../src/components/board/kanban-column.tsx | 155 ++++++++++++------ apps/web/src/components/board/task-board.tsx | 1 + 2 files changed, 109 insertions(+), 47 deletions(-) diff --git a/apps/web/src/components/board/kanban-column.tsx b/apps/web/src/components/board/kanban-column.tsx index 5b97bbb..489b895 100644 --- a/apps/web/src/components/board/kanban-column.tsx +++ b/apps/web/src/components/board/kanban-column.tsx @@ -9,12 +9,18 @@ import { import { CSS } from "@dnd-kit/utilities"; import { Check, - DotsSixVertical, + Circle, + CheckCircle, + Hourglass, + Eye, + Tray, DotsThree, PencilSimple, + Plus, Trash, X, } from "@phosphor-icons/react"; +import type { Icon as PhosphorIcon } from "@phosphor-icons/react"; import { useMemo, useRef, useState } from "react"; import { DropdownMenu, @@ -27,16 +33,51 @@ import { cn } from "~/lib/utils"; import { COLUMN_COLORS } from "./add-column"; import { DraggableTaskCard } from "./draggable-task-card"; +const COLUMN_META: Record< + string, + { icon: PhosphorIcon; description: string } +> = { + backlog: { + icon: Tray, + description: "Atividades que estão sendo refinadas", + }, + todo: { + icon: Circle, + description: "Atividades prontas para serem desenvolvidas", + }, + "in progress": { + icon: Hourglass, + description: "Atividades que estão em desenvolvimento", + }, + review: { + icon: Eye, + description: "Atividades aguardando revisão", + }, + done: { + icon: CheckCircle, + description: "Atividades finalizadas", + }, +}; + +function getColumnMeta(name: string, isCompleted: boolean) { + const key = name.toLowerCase(); + if (COLUMN_META[key]) return COLUMN_META[key]; + if (isCompleted) return { icon: CheckCircle, description: "" }; + return { icon: Circle, description: "" }; +} + interface KanbanColumnProps { column: Column; onDelete?: (id: string) => void; onUpdate?: (id: string, input: UpdateColumnInput) => void; + onAddTask?: (columnId: string) => void; } export function KanbanColumn({ column, onDelete, onUpdate, + onAddTask, }: KanbanColumnProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(column.name); @@ -78,6 +119,11 @@ export function KanbanColumn({ [column.tasks], ); + const { icon: StatusIcon, description } = getColumnMeta( + column.name, + column.isCompleted, + ); + const handleStartEdit = () => { setEditName(column.name); setEditColor(column.color ?? COLUMN_COLORS[0].color); @@ -171,7 +217,7 @@ export function KanbanColumn({ ref={setSortableNodeRef} style={style} className={cn( - "flex w-64 min-w-64 flex-col rounded-lg border border-border/50 bg-card/30 p-3 shadow-sm", + "flex w-64 min-w-64 flex-col rounded-lg p-3", isDragging && "opacity-50", )} > @@ -179,70 +225,85 @@ export function KanbanColumn({
- - {column.color && ( -
- )} {column.name} - + {column.tasks.length}
- {(onDelete || onUpdate) && ( - - e.stopPropagation()} +
+ {onAddTask && ( + + )} + + {(onDelete || onUpdate) && ( + + e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + + + + {onUpdate && ( + + + Edit column + + )} + {onDelete && ( + onDelete(column.id)} + > + + Delete column + + )} + + + )} +
+ {/* Description */} + {description && ( +

{description}

+ )} + {/* Task Drop Area */}
diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index a1227ca..1889b68 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -613,6 +613,7 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { column={column} onDelete={handleDeleteColumn} onUpdate={handleUpdateColumn} + onAddTask={() => setIsModalOpen(true)} /> ))} Date: Wed, 11 Feb 2026 23:30:57 -0300 Subject: [PATCH 03/13] refact: redesign task cards with priority icon, labels, and footer Replace priority dot with Warning icon, show all labels as badges, add issue identifier footer with assignee avatar. Remove lateral colored border, use ring hover effect instead. --- .../components/board/draggable-task-card.tsx | 114 ++++++++++-------- apps/web/src/components/board/task-card.tsx | 113 +++++++++-------- 2 files changed, 117 insertions(+), 110 deletions(-) diff --git a/apps/web/src/components/board/draggable-task-card.tsx b/apps/web/src/components/board/draggable-task-card.tsx index f4cd26b..5be74f0 100644 --- a/apps/web/src/components/board/draggable-task-card.tsx +++ b/apps/web/src/components/board/draggable-task-card.tsx @@ -2,7 +2,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { CheckCircle } from "@phosphor-icons/react"; +import { Warning } from "@phosphor-icons/react"; import { useRouter } from "next/navigation"; import { useRef } from "react"; import type { Task } from "~/lib/types"; @@ -13,12 +13,12 @@ interface DraggableTaskCardProps { isCompleted?: boolean; } -const priorityColors = { - HIGH: "#ef4444", - MEDIUM: "#f59e0b", - LOW: "#22c55e", - NONE: "transparent", -}; +const priorityConfig = { + HIGH: { color: "#ef4444", label: "High" }, + MEDIUM: { color: "#f59e0b", label: "Medium" }, + LOW: { color: "#22c55e", label: "Low" }, + NONE: null, +} as const; function getInitials(name: string): string { return name @@ -29,6 +29,15 @@ function getInitials(name: string): string { .slice(0, 2); } +function stringToColor(str: string): string { + const colors = ["#6366F1", "#E85A4F", "#32D583", "#FFB547", "#8B5CF6"]; + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; +} + export function DraggableTaskCard({ task, isCompleted = false, @@ -56,7 +65,7 @@ export function DraggableTaskCard({ transition, }; - const firstLabel = task.labels?.[0]; + const priority = priorityConfig[task.priority]; const handleMouseDown = () => { hasDragged.current = false; @@ -93,62 +102,65 @@ export function DraggableTaskCard({ onClick={handleClick} onKeyDown={handleKeyDown} className={cn( - "flex w-full flex-col gap-2 rounded-lg border p-3 text-left", + "flex w-full flex-col gap-2.5 rounded-lg p-3 text-left transition-all", isDragging - ? "border-foreground/20 border-dashed bg-accent/50 opacity-40" - : "cursor-grab border-border bg-card transition-colors hover:border-foreground/20 active:cursor-grabbing", + ? "border border-foreground/20 border-dashed bg-accent/50 opacity-40" + : "cursor-grab bg-card hover:ring-1 hover:ring-foreground/20 active:cursor-grabbing", + isCompleted && "opacity-60", )} > -
- {isCompleted ? ( - - ) : ( - task.priority !== "NONE" && ( -
- ) + {/* Title */} + - {task.title} - -
+ > + {task.title} + - {(firstLabel || task.assignee) && ( -
- {firstLabel && ( + {/* Indicators row */} + {(priority || task.labels.length > 0) && ( +
+ {priority && ( + + )} + {task.labels.map((label) => ( - {firstLabel.text} + {label.text} - )} - {task.assignee && ( -
- {getInitials(task.assignee.name)} -
- )} + ))}
)} + + {/* Footer */} +
+ + Issue #{task.order + 1} + + {task.assignee && ( +
+ {getInitials(task.assignee.name)} +
+ )} +
); } diff --git a/apps/web/src/components/board/task-card.tsx b/apps/web/src/components/board/task-card.tsx index ed597eb..dd74192 100644 --- a/apps/web/src/components/board/task-card.tsx +++ b/apps/web/src/components/board/task-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { Check } from "lucide-react"; +import { Warning } from "@phosphor-icons/react"; import Link from "next/link"; import type { Task } from "~/lib/types"; import { cn } from "~/lib/utils"; @@ -10,12 +10,12 @@ interface TaskCardProps { isCompleted?: boolean; } -const priorityColors = { - HIGH: "#E85A4F", - MEDIUM: "#FFB547", - LOW: "#32D583", - NONE: "#4A4A50", -}; +const priorityConfig = { + HIGH: { color: "#ef4444", label: "High" }, + MEDIUM: { color: "#f59e0b", label: "Medium" }, + LOW: { color: "#22c55e", label: "Low" }, + NONE: null, +} as const; function getInitials(name: string): string { return name @@ -36,73 +36,68 @@ function stringToColor(str: string): string { } export function TaskCard({ task, isCompleted = false }: TaskCardProps) { - const firstLabel = task.labels?.[0]; + const priority = priorityConfig[task.priority]; return ( - {/* Header */} -
- {isCompleted ? ( -
- -
- ) : ( -
+ {/* Title */} + - {task.title} - -
+ > + {task.title} + - {/* Description */} - {task.description && ( -

0) && ( +

+ {priority && ( + )} - > - {task.description} -

- )} - - {/* Footer */} - {(firstLabel || task.assignee) && ( -
- {firstLabel && ( + {task.labels.map((label) => ( - {firstLabel.text} + {label.text} - )} - {task.assignee && ( -
- - {getInitials(task.assignee.name)} - -
- )} + ))}
)} + + {/* Footer */} +
+ + Issue #{task.order + 1} + + {task.assignee && ( +
+ {getInitials(task.assignee.name)} +
+ )} +
); } From 0f2b87d1199bb2a714789782f1f913047208dd6e Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Wed, 11 Feb 2026 23:40:23 -0300 Subject: [PATCH 04/13] feat: add inline task creation inside kanban columns Add InlineTaskCreate component with input field and issue counter. Wire it into KanbanColumn and TaskBoard using existing useCreateTask mutation. --- apps/web/src/components/board/index.ts | 1 + .../components/board/inline-task-create.tsx | 43 +++++++++++++++++++ .../src/components/board/kanban-column.tsx | 14 ++++++ apps/web/src/components/board/task-board.tsx | 15 +++++++ 4 files changed, 73 insertions(+) create mode 100644 apps/web/src/components/board/inline-task-create.tsx diff --git a/apps/web/src/components/board/index.ts b/apps/web/src/components/board/index.ts index 00e0eaa..a12c29e 100644 --- a/apps/web/src/components/board/index.ts +++ b/apps/web/src/components/board/index.ts @@ -3,6 +3,7 @@ export { BoardHeader } from "./board-header"; export { CreateTaskModal } from "./create-task-modal"; export { DraggableTaskCard } from "./draggable-task-card"; export { EmptyBoard } from "./empty-board"; +export { InlineTaskCreate } from "./inline-task-create"; export { KanbanColumn } from "./kanban-column"; export { TaskBoard } from "./task-board"; export { TaskCard } from "./task-card"; diff --git a/apps/web/src/components/board/inline-task-create.tsx b/apps/web/src/components/board/inline-task-create.tsx new file mode 100644 index 0000000..2cf03a1 --- /dev/null +++ b/apps/web/src/components/board/inline-task-create.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; + +interface InlineTaskCreateProps { + columnId: string; + taskCount: number; + onSubmit: (title: string, columnId: string) => void; + isPending?: boolean; +} + +export function InlineTaskCreate({ + columnId, + taskCount, + onSubmit, + isPending, +}: InlineTaskCreateProps) { + const [title, setTitle] = useState(""); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && title.trim()) { + onSubmit(title.trim(), columnId); + setTitle(""); + } + }; + + return ( +
+ setTitle(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type the issue title here..." + disabled={isPending} + className="bg-transparent text-foreground text-sm placeholder:text-muted-foreground/40 focus:outline-none disabled:opacity-50" + /> + + Issue #{taskCount + 1} + +
+ ); +} diff --git a/apps/web/src/components/board/kanban-column.tsx b/apps/web/src/components/board/kanban-column.tsx index 489b895..0b16481 100644 --- a/apps/web/src/components/board/kanban-column.tsx +++ b/apps/web/src/components/board/kanban-column.tsx @@ -32,6 +32,7 @@ import type { Column, UpdateColumnInput } from "~/lib/types"; import { cn } from "~/lib/utils"; import { COLUMN_COLORS } from "./add-column"; import { DraggableTaskCard } from "./draggable-task-card"; +import { InlineTaskCreate } from "./inline-task-create"; const COLUMN_META: Record< string, @@ -71,6 +72,8 @@ interface KanbanColumnProps { onDelete?: (id: string) => void; onUpdate?: (id: string, input: UpdateColumnInput) => void; onAddTask?: (columnId: string) => void; + onCreateTask?: (title: string, columnId: string) => void; + isCreatingTask?: boolean; } export function KanbanColumn({ @@ -78,6 +81,8 @@ export function KanbanColumn({ onDelete, onUpdate, onAddTask, + onCreateTask, + isCreatingTask, }: KanbanColumnProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(column.name); @@ -316,6 +321,15 @@ export function KanbanColumn({ /> ))} + + {onCreateTask && ( + + )}
); diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index 1889b68..2727d38 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -516,6 +516,19 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { } }; + const handleInlineCreateTask = async (title: string, columnId: string) => { + try { + await createTaskMutation.mutateAsync({ + title, + columnId, + organizationId, + createdById: userId, + }); + } catch { + toast.error("Failed to create task"); + } + }; + const handleCreateColumn = async (name: string, color?: string) => { try { await createColumnMutation.mutateAsync({ @@ -614,6 +627,8 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { onDelete={handleDeleteColumn} onUpdate={handleUpdateColumn} onAddTask={() => setIsModalOpen(true)} + onCreateTask={handleInlineCreateTask} + isCreatingTask={createTaskMutation.isPending} /> ))} Date: Wed, 11 Feb 2026 23:47:16 -0300 Subject: [PATCH 05/13] refact: inline task creation as draft card triggered by + button Show a card-shaped draft with auto-focused input when clicking +, dismiss on Escape or blur, create task on Enter. --- .../components/board/inline-task-create.tsx | 33 +++++++++++++++---- .../src/components/board/kanban-column.tsx | 15 +++++---- apps/web/src/components/board/task-board.tsx | 1 - 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/board/inline-task-create.tsx b/apps/web/src/components/board/inline-task-create.tsx index 2cf03a1..ce3dcc5 100644 --- a/apps/web/src/components/board/inline-task-create.tsx +++ b/apps/web/src/components/board/inline-task-create.tsx @@ -1,11 +1,12 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; interface InlineTaskCreateProps { columnId: string; taskCount: number; onSubmit: (title: string, columnId: string) => void; + onCancel: () => void; isPending?: boolean; } @@ -13,31 +14,49 @@ export function InlineTaskCreate({ columnId, taskCount, onSubmit, + onCancel, isPending, }: InlineTaskCreateProps) { const [title, setTitle] = useState(""); + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + }, []); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && title.trim()) { onSubmit(title.trim(), columnId); - setTitle(""); + } else if (e.key === "Escape") { + onCancel(); + } + }; + + const handleBlur = () => { + if (!title.trim()) { + onCancel(); } }; return ( -
+
setTitle(e.target.value)} onKeyDown={handleKeyDown} + onBlur={handleBlur} placeholder="Type the issue title here..." disabled={isPending} - className="bg-transparent text-foreground text-sm placeholder:text-muted-foreground/40 focus:outline-none disabled:opacity-50" + className="bg-transparent font-medium text-foreground text-sm placeholder:text-muted-foreground/40 focus:outline-none disabled:opacity-50" /> - - Issue #{taskCount + 1} - + +
+ + Issue #{taskCount + 1} + +
); } diff --git a/apps/web/src/components/board/kanban-column.tsx b/apps/web/src/components/board/kanban-column.tsx index 0b16481..ca59c18 100644 --- a/apps/web/src/components/board/kanban-column.tsx +++ b/apps/web/src/components/board/kanban-column.tsx @@ -71,7 +71,6 @@ interface KanbanColumnProps { column: Column; onDelete?: (id: string) => void; onUpdate?: (id: string, input: UpdateColumnInput) => void; - onAddTask?: (columnId: string) => void; onCreateTask?: (title: string, columnId: string) => void; isCreatingTask?: boolean; } @@ -80,11 +79,11 @@ export function KanbanColumn({ column, onDelete, onUpdate, - onAddTask, onCreateTask, isCreatingTask, }: KanbanColumnProps) { const [isEditing, setIsEditing] = useState(false); + const [isCreating, setIsCreating] = useState(false); const [editName, setEditName] = useState(column.name); const [editColor, setEditColor] = useState( column.color ?? COLUMN_COLORS[0].color, @@ -247,12 +246,12 @@ export function KanbanColumn({
- {onAddTask && ( + {onCreateTask && (
diff --git a/apps/web/src/components/board/draggable-task-card.tsx b/apps/web/src/components/board/draggable-task-card.tsx index 5be74f0..502565c 100644 --- a/apps/web/src/components/board/draggable-task-card.tsx +++ b/apps/web/src/components/board/draggable-task-card.tsx @@ -2,7 +2,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { Warning } from "@phosphor-icons/react"; +import { Warning, UserCircleDashed } from "@phosphor-icons/react"; import { useRouter } from "next/navigation"; import { useRef } from "react"; import type { Task } from "~/lib/types"; @@ -102,10 +102,10 @@ export function DraggableTaskCard({ onClick={handleClick} onKeyDown={handleKeyDown} className={cn( - "flex w-full flex-col gap-2.5 rounded-lg p-3 text-left transition-all", + "flex w-full flex-col gap-3 rounded-xl border border-border/50 p-4 text-left transition-all", isDragging - ? "border border-foreground/20 border-dashed bg-accent/50 opacity-40" - : "cursor-grab bg-card hover:ring-1 hover:ring-foreground/20 active:cursor-grabbing", + ? "border-foreground/20 border-dashed bg-accent/50 opacity-40" + : "cursor-grab bg-card hover:border-foreground/20 active:cursor-grabbing", isCompleted && "opacity-60", )} > @@ -123,10 +123,10 @@ export function DraggableTaskCard({ {/* Indicators row */} {(priority || task.labels.length > 0) && ( -
+
{priority && ( @@ -134,12 +134,16 @@ export function DraggableTaskCard({ {task.labels.map((label) => ( + {label.text} ))} @@ -148,10 +152,10 @@ export function DraggableTaskCard({ {/* Footer */}
- - Issue #{task.order + 1} + + Issue #{task.order + 1} | FrontEnd - {task.assignee && ( + {task.assignee ? (
{getInitials(task.assignee.name)}
+ ) : ( + )}
diff --git a/apps/web/src/components/board/inline-task-create.tsx b/apps/web/src/components/board/inline-task-create.tsx index ce3dcc5..d5884b9 100644 --- a/apps/web/src/components/board/inline-task-create.tsx +++ b/apps/web/src/components/board/inline-task-create.tsx @@ -1,5 +1,6 @@ "use client"; +import { UserCircleDashed } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; interface InlineTaskCreateProps { @@ -39,7 +40,7 @@ export function InlineTaskCreate({ }; return ( -
+
-
- - Issue #{taskCount + 1} +
+ + Issue #{taskCount + 1} | FrontEnd +
); diff --git a/apps/web/src/components/board/kanban-column.tsx b/apps/web/src/components/board/kanban-column.tsx index 3543860..b2322e2 100644 --- a/apps/web/src/components/board/kanban-column.tsx +++ b/apps/web/src/components/board/kanban-column.tsx @@ -165,7 +165,7 @@ export function KanbanColumn({ if (isEditing) { return ( -
+
@@ -238,8 +238,8 @@ export function KanbanColumn({ >
diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index f154729..8988007 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -619,7 +619,7 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { items={columnIds} strategy={horizontalListSortingStrategy} > -
+
{filteredColumns.map((column) => ( @@ -60,10 +60,10 @@ export function TaskCard({ task, isCompleted = false }: TaskCardProps) { {/* Indicators row */} {(priority || task.labels.length > 0) && ( -
+
{priority && ( @@ -71,12 +71,16 @@ export function TaskCard({ task, isCompleted = false }: TaskCardProps) { {task.labels.map((label) => ( + {label.text} ))} @@ -85,10 +89,10 @@ export function TaskCard({ task, isCompleted = false }: TaskCardProps) { {/* Footer */}
- - Issue #{task.order + 1} + + Issue #{task.order + 1} | FrontEnd - {task.assignee && ( + {task.assignee ? (
{getInitials(task.assignee.name)}
+ ) : ( + )}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 2d7f3f0..e63c8f3 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -127,6 +127,14 @@ } } +@utility scrollbar-none { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} + ::view-transition-old(root), ::view-transition-new(root) { animation: none; From b639fee4bb1f54cd69560a70a635e54ac3de8f34 Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Thu, 12 Feb 2026 00:44:39 -0300 Subject: [PATCH 08/13] feat(server): add description field to column model and endpoints --- apps/server/src/modules/columns/create-column/schemas.ts | 2 ++ apps/server/src/modules/columns/create-column/use-case.ts | 1 + apps/server/src/modules/columns/get-columns/schemas.ts | 1 + apps/server/src/modules/columns/update-column/schemas.ts | 2 ++ apps/server/src/modules/columns/update-column/use-case.ts | 1 + packages/db/prisma/schema/task.prisma | 1 + 6 files changed, 8 insertions(+) diff --git a/apps/server/src/modules/columns/create-column/schemas.ts b/apps/server/src/modules/columns/create-column/schemas.ts index 47d5ed4..55a7dbd 100644 --- a/apps/server/src/modules/columns/create-column/schemas.ts +++ b/apps/server/src/modules/columns/create-column/schemas.ts @@ -3,6 +3,7 @@ import { zDate } from "@/shared/schemas/zod-date"; export const createColumnBodySchema = z.object({ name: z.string().min(1), + description: z.string().optional(), color: z.string().optional(), isCompleted: z.boolean().optional().default(false), }); @@ -11,6 +12,7 @@ export type CreateColumnInput = z.infer; export const createColumnResponseSchema = z.object({ name: z.string(), + description: z.string().nullable(), id: z.string(), createdAt: zDate, updatedAt: zDate, diff --git a/apps/server/src/modules/columns/create-column/use-case.ts b/apps/server/src/modules/columns/create-column/use-case.ts index 8e4c6d1..df6bdb6 100644 --- a/apps/server/src/modules/columns/create-column/use-case.ts +++ b/apps/server/src/modules/columns/create-column/use-case.ts @@ -13,6 +13,7 @@ export async function createColumnUseCase( return prisma.column.create({ data: { name: input.name, + description: input.description, color: input.color, order: lastColumn ? lastColumn.order + 1 : 0, isCompleted: input.isCompleted ?? false, diff --git a/apps/server/src/modules/columns/get-columns/schemas.ts b/apps/server/src/modules/columns/get-columns/schemas.ts index 0c8b794..3f49d2b 100644 --- a/apps/server/src/modules/columns/get-columns/schemas.ts +++ b/apps/server/src/modules/columns/get-columns/schemas.ts @@ -33,6 +33,7 @@ export const getColumnsSucessResponseSchema = z .array(), id: z.string(), name: z.string(), + description: z.string().nullable(), color: z.string().nullable(), order: z.number(), isCompleted: z.boolean(), diff --git a/apps/server/src/modules/columns/update-column/schemas.ts b/apps/server/src/modules/columns/update-column/schemas.ts index 2d37391..460e357 100644 --- a/apps/server/src/modules/columns/update-column/schemas.ts +++ b/apps/server/src/modules/columns/update-column/schemas.ts @@ -7,6 +7,7 @@ export const updateColumnParamsSchema = z.object({ export const updateColumnBodySchema = z.object({ name: z.string().min(1).optional(), + description: z.string().nullable().optional(), color: z.string().optional(), order: z.number().int().optional(), isCompleted: z.boolean().optional(), @@ -17,6 +18,7 @@ export type UpdateColumnInput = z.infer; export const updateColumnResponseSchema = z.object({ id: z.string(), name: z.string(), + description: z.string().nullable(), createdAt: zDate, updatedAt: zDate, organizationId: z.string(), diff --git a/apps/server/src/modules/columns/update-column/use-case.ts b/apps/server/src/modules/columns/update-column/use-case.ts index 8735cf8..5f70e0e 100644 --- a/apps/server/src/modules/columns/update-column/use-case.ts +++ b/apps/server/src/modules/columns/update-column/use-case.ts @@ -9,6 +9,7 @@ export async function updateColumnUseCase( where: { id }, data: { name: input.name, + description: input.description, color: input.color, order: input.order, isCompleted: input.isCompleted, diff --git a/packages/db/prisma/schema/task.prisma b/packages/db/prisma/schema/task.prisma index 65a636d..540c90d 100644 --- a/packages/db/prisma/schema/task.prisma +++ b/packages/db/prisma/schema/task.prisma @@ -8,6 +8,7 @@ enum TaskPriority { model Column { id String @id @default(auto()) @map("_id") @db.ObjectId name String + description String? color String? order Int @default(0) isCompleted Boolean @default(false) From 4789764a1f3a8d264d431a285ff64c3d8dc79b6c Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Thu, 12 Feb 2026 00:44:45 -0300 Subject: [PATCH 09/13] feat(web): add description to column types, default columns and hooks --- apps/web/src/hooks/board/use-column-mutations.ts | 1 + apps/web/src/lib/types.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/src/hooks/board/use-column-mutations.ts b/apps/web/src/hooks/board/use-column-mutations.ts index 9b45bc9..786cbd0 100644 --- a/apps/web/src/hooks/board/use-column-mutations.ts +++ b/apps/web/src/hooks/board/use-column-mutations.ts @@ -17,6 +17,7 @@ export function useCreateColumn( mutationFn: async (input: CreateColumnInput) => { const { data, error } = await api.columns.post({ name: input.name, + description: input.description, color: input.color, isCompleted: input.isCompleted ?? false, }); diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 76963e4..78cb970 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -44,6 +44,7 @@ export interface TaskWithDetails extends Task { export interface Column { id: string; name: string; + description: string | null; color: string | null; order: number; isCompleted: boolean; @@ -84,6 +85,7 @@ export interface MoveTaskInput { export interface CreateColumnInput { name: string; + description?: string; color?: string; isCompleted?: boolean; organizationId: string; @@ -91,16 +93,18 @@ export interface CreateColumnInput { export interface UpdateColumnInput { name?: string; + description?: string | null; color?: string; order?: number; isCompleted?: boolean; } export const DEFAULT_COLUMNS = [ - { name: "Backlog", color: "#6B6B70", isCompleted: false }, - { name: "In Progress", color: "#6366F1", isCompleted: false }, - { name: "Review", color: "#FFB547", isCompleted: false }, - { name: "Done", color: "#32D583", isCompleted: true }, + { name: "Backlog", description: "Tasks being refined and prioritized", color: "icon:backlog", isCompleted: false }, + { name: "Todo", description: "Tasks ready to be worked on", color: "icon:todo", isCompleted: false }, + { name: "In Progress", description: "Tasks currently being developed", color: "icon:in-progress", isCompleted: false }, + { name: "Review", description: "Tasks awaiting review", color: "icon:review", isCompleted: false }, + { name: "Done", description: "Completed tasks", color: "icon:done", isCompleted: true }, ] as const; export type TaskCreatedMessage = { From 0e46fd26aad856de1e5f02483b0f6fa7cd04be19 Mon Sep 17 00:00:00 2001 From: Luiz Castro Date: Thu, 12 Feb 2026 00:44:51 -0300 Subject: [PATCH 10/13] feat(web): auto-create default columns on org creation and remove EmptyBoard --- apps/web/src/components/board/task-board.tsx | 144 ++++++++---------- .../org/create-organization-form.tsx | 12 ++ 2 files changed, 74 insertions(+), 82 deletions(-) diff --git a/apps/web/src/components/board/task-board.tsx b/apps/web/src/components/board/task-board.tsx index 8988007..94f15a7 100644 --- a/apps/web/src/components/board/task-board.tsx +++ b/apps/web/src/components/board/task-board.tsx @@ -23,7 +23,6 @@ import { toast } from "sonner"; import { useColumns, useCreateColumn, - useCreateDefaultColumns, useCreateTask, useDeleteColumn, useMoveTask, @@ -38,12 +37,10 @@ import type { Task, UpdateColumnInput, } from "~/lib/types"; -import { DEFAULT_COLUMNS } from "~/lib/types"; import { AddColumn } from "./add-column"; import { BoardHeader } from "./board-header"; import { CreateTaskModal } from "./create-task-modal"; import { DraggableTaskCard } from "./draggable-task-card"; -import { EmptyBoard } from "./empty-board"; import { KanbanColumn } from "./kanban-column"; interface TaskBoardProps { @@ -138,8 +135,6 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { }, }); - const createDefaultColumnsMutation = useCreateDefaultColumns(organizationId); - const reorderColumnsMutation = useReorderColumns(organizationId, { onSuccess: (columns) => { sendColumnsReordered({ columns }); @@ -529,10 +524,11 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { } }; - const handleCreateColumn = async (name: string, color?: string) => { + const handleCreateColumn = async (name: string, color?: string, description?: string) => { try { await createColumnMutation.mutateAsync({ name, + description, color, organizationId, }); @@ -568,15 +564,6 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { } }; - const handleCreateDefaultColumns = async () => { - try { - await createDefaultColumnsMutation.mutateAsync([...DEFAULT_COLUMNS]); - toast.success("Default columns created successfully"); - } catch { - toast.error("Failed to create default columns"); - } - }; - if (isLoading) { return (
@@ -600,78 +587,71 @@ export function TaskBoard({ organizationId, userId }: TaskBoardProps) { issueCount={totalTasks} /> - {localColumns.length === 0 ? ( - - ) : ( - + - -
- {filteredColumns.map((column) => ( - - ))} - + {filteredColumns.map((column) => ( + + ))} + +
+
+ + + {activeTask && ( +
+
-
- - - {activeTask && ( -
- + )} + {activeColumn && ( +
+
+ {activeColumn.color && ( +
+ )} + + {activeColumn.name} + + + {activeColumn.tasks.length} +
- )} - {activeColumn && ( -
-
- {activeColumn.color && ( -
- )} - - {activeColumn.name} - - - {activeColumn.tasks.length} - -
-
- {activeColumn.tasks.length} tasks -
+
+ {activeColumn.tasks.length} tasks
- )} - - - )} +
+ )} + + Date: Thu, 12 Feb 2026 00:44:57 -0300 Subject: [PATCH 11/13] feat(web): add icon/color selector and description field to column forms --- apps/web/src/components/board/add-column.tsx | 143 +++++++++-- .../src/components/board/kanban-column.tsx | 223 +++++++++++++----- 2 files changed, 279 insertions(+), 87 deletions(-) diff --git a/apps/web/src/components/board/add-column.tsx b/apps/web/src/components/board/add-column.tsx index b6d473c..7d42352 100644 --- a/apps/web/src/components/board/add-column.tsx +++ b/apps/web/src/components/board/add-column.tsx @@ -1,14 +1,24 @@ "use client"; import { Check, Plus, X } from "@phosphor-icons/react"; +import type { ComponentType, SVGProps } from "react"; import { useRef, useState } from "react"; +import { + BacklogIcon, + DoneIcon, + InProgressIcon, + ReviewIcon, + TodoIcon, +} from "~/components/icons"; import { cn } from "~/lib/utils"; interface AddColumnProps { - onAdd: (name: string, color?: string) => void; + onAdd: (name: string, color?: string, description?: string) => void; isLoading?: boolean; } +type IconComponent = ComponentType>; + const COLUMN_COLORS = [ { id: "blue", color: "#3b82f6" }, { id: "yellow", color: "#eab308" }, @@ -20,22 +30,43 @@ const COLUMN_COLORS = [ { id: "cyan", color: "#06b6d4" }, ]; +const COLUMN_ICONS: { id: string; label: string; icon: IconComponent }[] = [ + { id: "backlog", label: "Backlog", icon: BacklogIcon }, + { id: "todo", label: "Todo", icon: TodoIcon }, + { id: "in-progress", label: "In Progress", icon: InProgressIcon }, + { id: "review", label: "Review", icon: ReviewIcon }, + { id: "done", label: "Done", icon: DoneIcon }, +]; + +type SelectionMode = "icon" | "color"; + export function AddColumn({ onAdd, isLoading }: AddColumnProps) { const [isEditing, setIsEditing] = useState(false); const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [mode, setMode] = useState("icon"); + const [selectedIcon, setSelectedIcon] = useState(COLUMN_ICONS[0].id); const [selectedColor, setSelectedColor] = useState(COLUMN_COLORS[0].color); const inputRef = useRef(null); const handleSubmit = () => { if (!name.trim()) return; - onAdd(name.trim(), selectedColor); + const colorValue = + mode === "icon" ? `icon:${selectedIcon}` : selectedColor; + onAdd(name.trim(), colorValue, description.trim() || undefined); setName(""); + setDescription(""); + setMode("icon"); + setSelectedIcon(COLUMN_ICONS[0].id); setSelectedColor(COLUMN_COLORS[0].color); setIsEditing(false); }; const handleCancel = () => { setName(""); + setDescription(""); + setMode("icon"); + setSelectedIcon(COLUMN_ICONS[0].id); setSelectedColor(COLUMN_COLORS[0].color); setIsEditing(false); }; @@ -66,30 +97,94 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) { className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" /> -
- Color -
- {COLUMN_COLORS.map((c) => ( - - ))} -
+ setDescription(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Description (optional)" + className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-xs placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" + /> + + {/* Mode toggle */} +
+ +
+ {mode === "icon" ? ( +
+ Icon +
+ {COLUMN_ICONS.map((item) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+ ) : ( +
+ Color +
+ {COLUMN_COLORS.map((c) => ( + + ))} +
+
+ )} +
- ))} -
+ setEditDescription(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Description (optional)" + className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-xs placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" + /> + + {/* Mode toggle */} +
+ +
+ {editMode === "icon" ? ( +
+ Icon +
+ {Object.entries(ICON_MAP).map(([key, Icon]) => ( + + ))} +
+
+ ) : ( +
+ Color +
+ {COLUMN_COLORS.map((c) => ( + + ))} +
+
+ )} +
- + - + Delete task @@ -285,7 +281,7 @@ export default function TaskDetailsPage({ params }: PageProps) { Due Date
- + {formatDate(task.dueDate)} @@ -310,9 +306,7 @@ export default function TaskDetailsPage({ params }: PageProps) {
- - Status - + Status
{selectedColor === c.color && ( - + )} ))} @@ -199,7 +199,7 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) { onClick={handleCancel} className="flex size-8 items-center justify-center rounded-lg border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" > - +
@@ -212,7 +212,7 @@ export function AddColumn({ onAdd, isLoading }: AddColumnProps) { onClick={handleStartEditing} className="flex h-9 w-64 min-w-64 items-center justify-center gap-1.5 rounded-lg border border-border border-dashed text-muted-foreground transition-colors hover:border-foreground/30 hover:bg-accent hover:text-foreground" > - + Add column ); diff --git a/apps/web/src/components/board/board-header.tsx b/apps/web/src/components/board/board-header.tsx index 9554ee8..64e16f4 100644 --- a/apps/web/src/components/board/board-header.tsx +++ b/apps/web/src/components/board/board-header.tsx @@ -2,80 +2,80 @@ import { useState } from "react"; import { - Cube, - UsersThree, - CalendarBlank, - SlidersHorizontal, - Bell, + CubeIcon, + UsersThreeIcon, + CalendarIcon, + SlidersHorizontalIcon, + BellIcon, } from "@phosphor-icons/react"; const FILTER_TABS = [ - { id: "projects", label: "Projects", icon: Cube }, - { id: "teams", label: "Teams", icon: UsersThree }, - { id: "date", label: "Date", icon: CalendarBlank }, + { id: "projects", label: "Projects", icon: CubeIcon }, + { id: "teams", label: "Teams", icon: UsersThreeIcon }, + { id: "date", label: "Date", icon: CalendarIcon }, ] as const; type FilterTab = (typeof FILTER_TABS)[number]["id"]; interface BoardHeaderProps { - title: string; - issueCount: number; + title: string; + issueCount: number; } export function BoardHeader({ title, issueCount }: BoardHeaderProps) { - const [activeTab, setActiveTab] = useState("projects"); + const [activeTab, setActiveTab] = useState("projects"); - return ( -
-
-
-

- {title} -

- - {issueCount} Issues - -
+ return ( +
+
+
+

+ {title} +

+ + {issueCount} Issues + +
- -
+ +
-
-
- {FILTER_TABS.map((tab) => { - const Icon = tab.icon; - const isActive = activeTab === tab.id; - return ( - - ); - })} -
+
+
+ {FILTER_TABS.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} +
- -
-
- ); + +
+
+ ); } diff --git a/apps/web/src/components/board/create-task-modal.tsx b/apps/web/src/components/board/create-task-modal.tsx index ff74cce..7b730ae 100644 --- a/apps/web/src/components/board/create-task-modal.tsx +++ b/apps/web/src/components/board/create-task-modal.tsx @@ -1,373 +1,367 @@ "use client"; -import { - CalendarBlank, - CaretDown, - Check, - Plus, - X, -} from "@phosphor-icons/react"; +import { CalendarIcon, CaretDownIcon, CheckIcon, PlusIcon, XIcon } from "@phosphor-icons/react"; import { useState } from "react"; import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "~/components/ui/dialog"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import type { Column, CreateTaskInput, TaskLabel } from "~/lib/types"; interface CreateTaskModalProps { - isOpen: boolean; - onClose: () => void; - columns: Column[]; - onSubmit: ( - data: Omit, - ) => Promise; + isOpen: boolean; + onClose: () => void; + columns: Column[]; + onSubmit: ( + data: Omit, + ) => Promise; } interface TagProps { - text: string; - color: string; - onRemove?: () => void; + text: string; + color: string; + onRemove?: () => void; } const priorities = [ - { id: "HIGH" as const, label: "High", color: "#ef4444" }, - { id: "MEDIUM" as const, label: "Medium", color: "#f59e0b" }, - { id: "LOW" as const, label: "Low", color: "#22c55e" }, - { id: "NONE" as const, label: "None", color: "transparent" }, + { id: "HIGH" as const, label: "High", color: "#ef4444" }, + { id: "MEDIUM" as const, label: "Medium", color: "#f59e0b" }, + { id: "LOW" as const, label: "Low", color: "#22c55e" }, + { id: "NONE" as const, label: "None", color: "transparent" }, ]; const dueDates = [ - { - id: "today", - label: "Today", - getValue: () => new Date().toISOString(), - }, - { - id: "tomorrow", - label: "Tomorrow", - getValue: () => new Date(Date.now() + 86400000).toISOString(), - }, - { - id: "next-week", - label: "Next Week", - getValue: () => new Date(Date.now() + 7 * 86400000).toISOString(), - }, - { - id: "next-month", - label: "Next Month", - getValue: () => new Date(Date.now() + 30 * 86400000).toISOString(), - }, - { - id: "no-date", - label: "No due date", - getValue: () => undefined, - }, + { + id: "today", + label: "Today", + getValue: () => new Date().toISOString(), + }, + { + id: "tomorrow", + label: "Tomorrow", + getValue: () => new Date(Date.now() + 86400000).toISOString(), + }, + { + id: "next-week", + label: "Next Week", + getValue: () => new Date(Date.now() + 7 * 86400000).toISOString(), + }, + { + id: "next-month", + label: "Next Month", + getValue: () => new Date(Date.now() + 30 * 86400000).toISOString(), + }, + { + id: "no-date", + label: "No due date", + getValue: () => undefined, + }, ]; function Tag({ text, color, onRemove }: TagProps) { - return ( -
- - {text} - - {onRemove && ( - - )} -
- ); + return ( +
+ + {text} + + {onRemove && ( + + )} +
+ ); } export function CreateTaskModal({ - isOpen, - onClose, - columns, - onSubmit, + isOpen, + onClose, + columns, + onSubmit, }: CreateTaskModalProps) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [selectedColumn, setSelectedColumn] = useState( - columns[0] ?? null, - ); - const [priority, setPriority] = useState(priorities[3]); - const [dueDate, setDueDate] = useState(dueDates[4]); - const [tags, setTags] = useState([]); - const [isSubmitting, setIsSubmitting] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [selectedColumn, setSelectedColumn] = useState( + columns[0] ?? null, + ); + const [priority, setPriority] = useState(priorities[3]); + const [dueDate, setDueDate] = useState(dueDates[4]); + const [tags, setTags] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); - const resetForm = () => { - setName(""); - setDescription(""); - setSelectedColumn(columns[0] ?? null); - setPriority(priorities[3]); - setDueDate(dueDates[4]); - setTags([]); - }; + const resetForm = () => { + setName(""); + setDescription(""); + setSelectedColumn(columns[0] ?? null); + setPriority(priorities[3]); + setDueDate(dueDates[4]); + setTags([]); + }; - const handleSubmit = async () => { - if (!name.trim() || !selectedColumn) return; + const handleSubmit = async () => { + if (!name.trim() || !selectedColumn) return; - setIsSubmitting(true); - try { - await onSubmit({ - title: name, - description: description || undefined, - priority: priority.id, - dueDate: dueDate.getValue(), - labels: tags.length > 0 ? tags : undefined, - columnId: selectedColumn.id, - }); - resetForm(); - onClose(); - } finally { - setIsSubmitting(false); - } - }; + setIsSubmitting(true); + try { + await onSubmit({ + title: name, + description: description || undefined, + priority: priority.id, + dueDate: dueDate.getValue(), + labels: tags.length > 0 ? tags : undefined, + columnId: selectedColumn.id, + }); + resetForm(); + onClose(); + } finally { + setIsSubmitting(false); + } + }; - const removeTag = (index: number) => { - setTags(tags.filter((_, i) => i !== index)); - }; + const removeTag = (index: number) => { + setTags(tags.filter((_, i) => i !== index)); + }; - const handleClose = () => { - resetForm(); - onClose(); - }; + const handleClose = () => { + resetForm(); + onClose(); + }; - return ( - !open && handleClose()}> - - - - Create new task - - - - - + return ( + !open && handleClose()}> + + + + Create new task + + + + + -
-
- - setName(e.target.value)} - placeholder="Enter task name..." - className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" - /> -
+
+
+ + setName(e.target.value)} + placeholder="Enter task name..." + className="h-9 rounded-lg border border-border bg-background px-3 text-foreground text-sm placeholder:text-muted-foreground focus:border-foreground/30 focus:outline-none" + /> +
-
- -