"use client"; import { useState } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useTheme } from "next-themes"; import { Archive, ClipboardList, GripVertical, MoreVertical, Pencil, Plus, Sparkles, StickyNote, Trash2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { TodoCheckbox } from "@/components/board/todo-checkbox"; import { MouseFollowTooltip } from "@/components/board/mouse-follow-tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { getBrighterColor, getComplementaryColor, getGroupColor } from "@/lib/colors"; import { useBoard } from "@/components/board/board-context"; import { useBoardView } from "@/components/board/board-view-provider"; import { NotesDialog } from "@/components/board/notes-dialog"; import { TodoAiDialog } from "@/components/board/todo-ai-dialog"; import { TodoCreateDialog } from "@/components/board/todo-create-dialog"; import { TodoCreatePopover } from "@/components/board/todo-create-popover"; import { TodoEditDialog } from "@/components/board/todo-edit-dialog"; import { TodoProgressPie } from "@/components/board/todo-progress-pie"; import { EditGroupDialog } from "@/components/board/edit-group-dialog"; import { StatusUpdateDialog } from "@/components/board/status-update-dialog"; import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog"; import type { GroupDTO, TodoDTO } from "@/types/board"; // Compact's "+" -- sits inline on the last visible to-do row (its sibling // text button is flex-1, so this shrink-0 trigger naturally lands at the // row's right edge) instead of on a row of its own. function AddTodoMenu({ group, aiConfigured, onAddClick, onAiClick, }: { group: GroupDTO; aiConfigured: boolean; onAddClick: () => void; onAiClick: () => void; }) { return ( } /> Add to-do {aiConfigured && ( Add using AI )} ); } export function GroupCard({ group }: { group: GroupDTO }) { const { toggleTodoDone, removeGroup, archiveGroup, aiConfigured } = useBoard(); const { view } = useBoardView(); const compact = view === "compact"; const { resolvedTheme } = useTheme(); const [editingTodo, setEditingTodo] = useState(null); const [editOpen, setEditOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [todoAiOpen, setTodoAiOpen] = useState(false); const [todoCreateOpen, setTodoCreateOpen] = useState(false); const [statusUpdateOpen, setStatusUpdateOpen] = useState(false); const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, isOver, } = useSortable({ id: group.id, data: { type: "group", categoryId: group.categoryId }, }); // Another card is being dragged and is currently hovering over this one // -- signal "it'll land near here" with a ring, distinct from this // card's own accent-colored border. (isDragging guards against this // card ever ringing itself while it's the one being moved.) const isDropTarget = isOver && !isDragging; const color = getGroupColor(group.color); const isDark = resolvedTheme === "dark"; const borderColor = isDark ? color.dark : color.light; // A subdued tint of the same stroke color, blended into the theme's own // card surface -- not a fixed pastel, so it automatically stays correct // if the neutral card token ever changes, and needs no separate // light/dark background palette to hand-tune. const backgroundColor = isDark ? `color-mix(in srgb, ${color.dark} 18%, var(--card))` : `color-mix(in srgb, ${color.light} 12%, var(--card))`; // Opposing hue from the group's own color, for the progress pie below -- // so it reads as a distinct accent rather than blending into the border. const pieAccentColor = getComplementaryColor( isDark ? color.dark : color.light, isDark ? "dark" : "light" ); // Un-checked to-do text: a brighter tint of the group's own color rather // than the default (near-white in dark mode) foreground, so the group // title reads as the most prominent thing on the card. const todoTextColor = getBrighterColor(borderColor, isDark ? "dark" : "light"); // Only offer archiving once there's actually something done -- a group // with no to-dos yet, or with any still open, isn't "finished" yet. const canArchive = group.todos.length > 0 && group.todos.every((t) => t.completed); const completedCount = group.todos.filter((t) => t.completed).length; // Compact only ever surfaces open work -- checked-off todos are dropped // from the list entirely rather than shown crossed-out. const visibleTodos = compact ? group.todos.filter((t) => !t.completed) : group.todos; return ( <>

{group.title}

} /> setEditOpen(true)}> Edit {aiConfigured && ( setStatusUpdateOpen(true)}> Status Update )} setConfirmOpen(true)}> Delete group
{compact ? ( // Defensive fallback -- shouldn't normally happen, since // CategoryLane already hides a compact group with zero visible // to-dos, but keeps the "+" reachable if that ever changes. visibleTodos.length === 0 && (
setTodoCreateOpen(true)} onAiClick={() => setTodoAiOpen(true)} aiConfigured={aiConfigured} />
) ) : ( <>
{aiConfigured && ( )} {canArchive && ( )} )}
{editingTodo && ( !open && setEditingTodo(null)} /> )} removeGroup(group.id, group.categoryId)} /> ); }