"use client"; import { useState } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Archive, ChevronRight, ClipboardList, GripVertical, MoreVertical, Pencil, Plus, Sparkles, StickyNote, Trash2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; 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 } from "@/lib/colors"; import { isDarkTheme, useGroupColor } from "@/components/theme/use-dark-theme"; import { useBoard } from "@/components/board/board-context"; import { useBoardView } from "@/components/board/board-view-provider"; import { useHoldOnComplete } from "@/components/hold-on-complete"; 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 { holds, beginHold, cancelHold } = useHoldOnComplete(); const compact = view === "compact"; // True for any theme whose surfaces are dark -- Dark and Ocean both use // their `dark` color variant (see lib/colors.ts); light themes use `light`. const isDark = isDarkTheme(); 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 [expandedWhileComplete, setExpandedWhileComplete] = 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; // The group's color for the ACTIVE theme: themes with their own palette // (cyberpunk, blueprint, vaporwave, notebook, starfield) retint the card, // every other theme keeps the base GROUP_COLORS. Falls back to the base // color pre-mount so the server render matches. const color = useGroupColor(group.color); const borderColor = isDark ? color.dark : color.light; // The card fill is a calm, hand-picked tint of the same stroke color // (`soft` / `softDark` in lib/colors.ts), blended a little into the theme's // own card surface so it sits naturally on any theme -- the 85% weight // keeps the tint in charge while still letting a tinted theme surface show // through. Dark themes use `softDark`: the same hue at a dark-surface // lightness, instead of the near-white pastel of `soft`. const backgroundColor = `color-mix(in srgb, ${isDark ? color.softDark : color.soft} 85%, 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 surfaces open work, plus anything still mid-hold after being // freshly checked off (see useCompactHold) -- it stays on screen, // crossed out, through its fade before actually dropping out of the list. const visibleTodos = compact ? group.todos.filter((t) => !t.completed || holds.has(t.id)) : group.todos; // The "+" rides on the last still-open row (see isLastRow below), not // necessarily the last row overall -- a held to-do can be lingering at // the tail while it fades out. -1 when every visible row is mid-hold, // which the fallback block below covers. let lastOpenTodoIndex = -1; if (compact) { visibleTodos.forEach((t, i) => { if (!t.completed) lastOpenTodoIndex = i; }); } // In the default view, a fully-checked-off group collapses down to just // its header (title, notes icon, kebab menu) with a disclosure triangle // in place of the drag-grip dots, so a board full of "done" groups // doesn't stay as visually loud as one still full of open work. The // triangle re-expands it back to the normal full card. Compact already // hides finished groups outright (see CategoryLane), so this only // applies to the default view. const showDisclosure = !compact && canArchive; // Reset back to collapsed each time the group freshly becomes fully // done, rather than remembering a stale expanded choice from its last // completion cycle. Adjusting state during render (comparing against // last render's value) instead of in a useEffect avoids an extra render // pass -- see https://react.dev/learn/you-might-not-need-an-effect. const [wasComplete, setWasComplete] = useState(canArchive); if (canArchive !== wasComplete) { setWasComplete(canArchive); if (canArchive) setExpandedWhileComplete(false); } const isCollapsed = showDisclosure && !expandedWhileComplete; return ( <>
{showDisclosure ? ( ) : ( )}

{group.title}

} /> setEditOpen(true)}> Edit {aiConfigured && ( setStatusUpdateOpen(true)}> Status Update )} setConfirmOpen(true)}> Delete group
{!isCollapsed && ( <> {compact ? ( // No open row to ride on -- either every to-do here is // mid-hold after being freshly checked off (about to clear // the group entirely once its fade finishes), or, as a // defensive fallback that shouldn't otherwise happen, // CategoryLane failed to hide an already-empty group. lastOpenTodoIndex === -1 && (
setTodoCreateOpen(true)} onAiClick={() => setTodoAiOpen(true)} aiConfigured={aiConfigured} />
) ) : ( <>
{aiConfigured && ( )} {(canArchive || group.todos.length > 0) && (
{canArchive && ( archiveGroup(group.id, group.categoryId)} aria-label="Archive group" > } /> Archive (all to-dos done) )} {/* pr-2.5: the corner progress pie (TodoProgressPie) hugs the card's right edge right here, so the label keeps a little room of its own -- together the two clear each other without either spilling past the lane's edge. */} {completedCount}/{group.todos.length} done
)} )} )} {/* Vaporwave's VHS tracking band sweeps inside this clip box (see .vw-glitch-clip in app/globals.css) instead of the card itself using overflow: hidden -- the card must stay overflow-visible so the progress pie can poke past the bottom-right corner without being clipped by it or the card's rounded corner. display:none (and thus inert) in every other theme. */}
{editingTodo && ( !open && setEditingTodo(null)} /> )} removeGroup(group.id, group.categoryId)} /> ); }