mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
Major fixes: - Fixed wagmi/Web3 SSR errors by lazy-loading config in Web3Provider - Added SidebarProvider to sidebar-demo components - Added 'use client' to 14 demo files with hooks/state/event handlers - Temporarily disabled force-static on blocks page Build progress: 0 → 258/1035 pages (25%) before hitting /blocks error Remaining issue: - /blocks page has function-passing error during static generation Package versions confirmed: - React 19.2.0 ✅ - Next.js 16.0.0 ✅ - Tailwind CSS 4.1.14 ✅
15 lines
22 KiB
JSON
15 lines
22 KiB
JSON
{
|
|
"name": "kanban",
|
|
"description": "Drag and drop Kanban board",
|
|
"dependencies": [
|
|
"@dnd-kit/sortable",
|
|
"@dnd-kit/core"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "kanban.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n closestCorners,\n DndContext,\n DragEndEvent,\n DragOverEvent,\n DragOverlay,\n DragStartEvent,\n PointerSensor,\n UniqueIdentifier,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\"\nimport {\n arrayMove,\n SortableContext,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n Calendar,\n Edit3,\n MoreHorizontal,\n Plus,\n Search,\n User,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { Badge } from \"./badge\"\nimport { Button } from \"./button\"\nimport { Card, CardContent, CardHeader } from \"./card\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"./dialog\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\nimport { Input } from \"./input\"\nimport { Label } from \"./label\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"./select\"\nimport { Textarea } from \"./textarea\"\n\n// Types\nexport interface KanbanCard {\n id: string\n title: string\n description?: string\n labels?: KanbanLabel[]\n priority?: \"low\" | \"medium\" | \"high\" | \"urgent\"\n assignee?: string\n dueDate?: string\n createdAt: string\n updatedAt: string\n}\n\nexport interface KanbanLabel {\n id: string\n name: string\n color: string\n}\n\nexport interface KanbanColumn {\n id: string\n title: string\n cards: KanbanCard[]\n limit?: number\n}\n\nexport interface KanbanBoard {\n id: string\n title: string\n columns: KanbanColumn[]\n labels: KanbanLabel[]\n}\n\n// Priority variants\nconst priorityVariants = cva(\n \"inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold\",\n {\n variants: {\n priority: {\n low: \"border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-400\",\n medium:\n \"border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400\",\n high: \"border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800 dark:bg-orange-900/20 dark:text-orange-400\",\n urgent:\n \"border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400\",\n },\n },\n defaultVariants: {\n priority: \"low\",\n },\n }\n)\n\n// Sortable Card Component\nfunction SortableCard({\n card,\n onEdit,\n onDelete,\n}: {\n card: KanbanCard\n onEdit: (card: KanbanCard) => void\n onDelete: (cardId: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: card.id })\n\n const style = {\n transform: CSS.Transform.toString(transform),\n transition,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n {...attributes}\n {...listeners}\n className={cn(\n \"cursor-grab active:cursor-grabbing\",\n isDragging && \"opacity-50\"\n )}\n >\n <KanbanCardComponent card={card} onEdit={onEdit} onDelete={onDelete} />\n </div>\n )\n}\n\n// Card Component\nfunction KanbanCardComponent({\n card,\n onEdit,\n onDelete,\n}: {\n card: KanbanCard\n onEdit: (card: KanbanCard) => void\n onDelete: (cardId: string) => void\n}) {\n return (\n <Card className=\"mb-3 group hover:shadow-md transition-shadow\">\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-sm leading-tight mb-2 break-words\">\n {card.title}\n </h4>\n {card.description && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">\n {card.description}\n </p>\n )}\n\n {/* Labels */}\n {card.labels && card.labels.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mb-2\">\n {card.labels.map((label) => (\n <Badge\n key={label.id}\n variant=\"outline\"\n className=\"text-xs h-5\"\n style={{\n backgroundColor: `${label.color}20`,\n borderColor: label.color,\n color: label.color,\n }}\n >\n {label.name}\n </Badge>\n ))}\n </div>\n )}\n\n {/* Priority and Meta */}\n <div className=\"flex items-center justify-between text-xs\">\n <div className=\"flex items-center gap-2\">\n {card.priority && (\n <span\n className={cn(\n priorityVariants({ priority: card.priority })\n )}\n >\n {card.priority}\n </span>\n )}\n {card.assignee && (\n <div className=\"flex items-center gap-1 text-muted-foreground\">\n <User className=\"h-3 w-3\" />\n <span>{card.assignee}</span>\n </div>\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {card.dueDate && (\n <div className=\"flex items-center gap-1 text-muted-foreground\">\n <Calendar className=\"h-3 w-3\" />\n <span>{new Date(card.dueDate).toLocaleDateString()}</span>\n </div>\n )}\n\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n <MoreHorizontal className=\"h-3 w-3\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => onEdit(card)}>\n <Edit3 className=\"h-4 w-4 mr-2\" />\n Edit\n </DropdownMenuItem>\n <DropdownMenuItem\n onClick={() => onDelete(card.id)}\n className=\"text-destructive\"\n >\n <X className=\"h-4 w-4 mr-2\" />\n Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n </div>\n </div>\n </CardHeader>\n </Card>\n )\n}\n\n// Column Component\nfunction KanbanColumnComponent({\n column,\n onAddCard,\n onEditCard,\n onDeleteCard,\n}: {\n column: KanbanColumn\n onAddCard: (columnId: string) => void\n onEditCard: (card: KanbanCard) => void\n onDeleteCard: (cardId: string) => void\n}) {\n const { setNodeRef } = useSortable({ id: column.id })\n\n return (\n <div\n ref={setNodeRef}\n className=\"bg-muted/50 rounded-lg p-4 min-w-[280px] max-w-[280px] flex flex-col\"\n >\n {/* Column Header */}\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-sm\">{column.title}</h3>\n <Badge variant=\"secondary\" className=\"text-xs\">\n {column.cards.length}\n {column.limit && `/${column.limit}`}\n </Badge>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => onAddCard(column.id)}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {/* Cards Container */}\n <SortableContext\n items={column.cards.map((card) => card.id)}\n strategy={verticalListSortingStrategy}\n >\n <div className=\"flex-1 min-h-[200px]\">\n {column.cards.map((card) => (\n <SortableCard\n key={card.id}\n card={card}\n onEdit={onEditCard}\n onDelete={onDeleteCard}\n />\n ))}\n </div>\n </SortableContext>\n </div>\n )\n}\n\n// Card Form Dialog\nfunction CardFormDialog({\n open,\n onOpenChange,\n card,\n onSave,\n labels,\n}: {\n open: boolean\n onOpenChange: (open: boolean) => void\n card?: KanbanCard\n onSave: (cardData: Partial<KanbanCard>) => void\n labels: KanbanLabel[]\n}) {\n const [formData, setFormData] = React.useState({\n title: card?.title || \"\",\n description: card?.description || \"\",\n priority: card?.priority || \"low\",\n assignee: card?.assignee || \"\",\n dueDate: card?.dueDate || \"\",\n labels: card?.labels?.map((l) => l.id) || [],\n })\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault()\n if (!formData.title.trim()) return\n\n onSave({\n ...card,\n ...formData,\n labels: labels.filter((l) => formData.labels.includes(l.id)),\n updatedAt: new Date().toISOString(),\n })\n onOpenChange(false)\n }\n\n React.useEffect(() => {\n if (card) {\n setFormData({\n title: card.title,\n description: card.description || \"\",\n priority: card.priority || \"low\",\n assignee: card.assignee || \"\",\n dueDate: card.dueDate || \"\",\n labels: card.labels?.map((l) => l.id) || [],\n })\n } else {\n setFormData({\n title: \"\",\n description: \"\",\n priority: \"low\",\n assignee: \"\",\n dueDate: \"\",\n labels: [],\n })\n }\n }, [card])\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{card ? \"Edit Card\" : \"Create Card\"}</DialogTitle>\n <DialogDescription>\n {card\n ? \"Update the card details below.\"\n : \"Create a new card for your kanban board.\"}\n </DialogDescription>\n </DialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div>\n <Label htmlFor=\"title\">Title *</Label>\n <Input\n id=\"title\"\n value={formData.title}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, title: e.target.value }))\n }\n placeholder=\"Enter card title...\"\n required\n />\n </div>\n\n <div>\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={formData.description}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n description: e.target.value,\n }))\n }\n placeholder=\"Enter card description...\"\n rows={3}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"priority\">Priority</Label>\n <Select\n value={formData.priority}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, priority: value as any }))\n }\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"low\">Low</SelectItem>\n <SelectItem value=\"medium\">Medium</SelectItem>\n <SelectItem value=\"high\">High</SelectItem>\n <SelectItem value=\"urgent\">Urgent</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div>\n <Label htmlFor=\"assignee\">Assignee</Label>\n <Input\n id=\"assignee\"\n value={formData.assignee}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, assignee: e.target.value }))\n }\n placeholder=\"Assign to...\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"dueDate\">Due Date</Label>\n <Input\n id=\"dueDate\"\n type=\"date\"\n value={formData.dueDate}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, dueDate: e.target.value }))\n }\n />\n </div>\n\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n >\n Cancel\n </Button>\n <Button type=\"submit\">{card ? \"Update\" : \"Create\"} Card</Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\n// Main Kanban Board Component\nexport interface KanbanBoardProps {\n board: KanbanBoard\n onUpdateBoard: (board: KanbanBoard) => void\n className?: string\n}\n\nexport function KanbanBoard({\n board,\n onUpdateBoard,\n className,\n}: KanbanBoardProps) {\n const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null)\n const [searchQuery, setSearchQuery] = React.useState(\"\")\n const [cardFormOpen, setCardFormOpen] = React.useState(false)\n const [editingCard, setEditingCard] = React.useState<KanbanCard | undefined>(\n undefined\n )\n const [activeColumnId, setActiveColumnId] = React.useState<string>(\"\")\n\n const sensors = useSensors(\n useSensor(PointerSensor, {\n activationConstraint: {\n distance: 8,\n },\n })\n )\n\n // Filter cards based on search query\n const filteredBoard = React.useMemo(() => {\n if (!searchQuery.trim()) return board\n\n const filtered = {\n ...board,\n columns: board.columns.map((column) => ({\n ...column,\n cards: column.cards.filter(\n (card) =>\n card.title.toLowerCase().includes(searchQuery.toLowerCase()) ||\n card.description\n ?.toLowerCase()\n .includes(searchQuery.toLowerCase()) ||\n card.labels?.some((label) =>\n label.name.toLowerCase().includes(searchQuery.toLowerCase())\n )\n ),\n })),\n }\n return filtered\n }, [board, searchQuery])\n\n const handleDragStart = (event: DragStartEvent) => {\n const { active } = event\n const card = board.columns\n .flatMap((col) => col.cards)\n .find((card) => card.id === active.id)\n setActiveCard(card || null)\n }\n\n const handleDragOver = (event: DragOverEvent) => {\n const { active, over } = event\n if (!over) return\n\n const activeId = active.id\n const overId = over.id\n\n // Find the containers\n const activeContainer = findContainer(activeId)\n const overContainer = findContainer(overId)\n\n if (\n !activeContainer ||\n !overContainer ||\n activeContainer === overContainer\n ) {\n return\n }\n\n // Move card between columns\n const updatedBoard = { ...board }\n const activeColumnIndex = updatedBoard.columns.findIndex(\n (col) => col.id === activeContainer\n )\n const overColumnIndex = updatedBoard.columns.findIndex(\n (col) => col.id === overContainer\n )\n\n const activeColumn = updatedBoard.columns[activeColumnIndex]\n const overColumn = updatedBoard.columns[overColumnIndex]\n\n const activeCardIndex = activeColumn.cards.findIndex(\n (card) => card.id === activeId\n )\n const [movedCard] = activeColumn.cards.splice(activeCardIndex, 1)\n\n // Check column limit\n if (overColumn.limit && overColumn.cards.length >= overColumn.limit) {\n return\n }\n\n overColumn.cards.push(movedCard)\n onUpdateBoard(updatedBoard)\n }\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event\n setActiveCard(null)\n\n if (!over) return\n\n const activeId = active.id\n const overId = over.id\n\n const activeContainer = findContainer(activeId)\n const overContainer = findContainer(overId)\n\n if (!activeContainer || !overContainer) return\n\n const activeColumnIndex = board.columns.findIndex(\n (col) => col.id === activeContainer\n )\n const activeColumn = board.columns[activeColumnIndex]\n\n if (activeContainer === overContainer) {\n // Reordering within the same column\n const oldIndex = activeColumn.cards.findIndex(\n (card) => card.id === activeId\n )\n const newIndex = activeColumn.cards.findIndex(\n (card) => card.id === overId\n )\n\n if (oldIndex !== newIndex) {\n const updatedBoard = { ...board }\n updatedBoard.columns[activeColumnIndex].cards = arrayMove(\n activeColumn.cards,\n oldIndex,\n newIndex\n )\n onUpdateBoard(updatedBoard)\n }\n }\n }\n\n const findContainer = (id: UniqueIdentifier): string | undefined => {\n // Check if it's a column\n if (board.columns.some((col) => col.id === id)) {\n return id as string\n }\n\n // Find which column contains this card\n return board.columns.find((col) => col.cards.some((card) => card.id === id))\n ?.id\n }\n\n const handleAddCard = (columnId: string) => {\n setActiveColumnId(columnId)\n setEditingCard(undefined)\n setCardFormOpen(true)\n }\n\n const handleEditCard = (card: KanbanCard) => {\n setEditingCard(card)\n setActiveColumnId(\"\")\n setCardFormOpen(true)\n }\n\n const handleSaveCard = (cardData: Partial<KanbanCard>) => {\n const updatedBoard = { ...board }\n\n if (editingCard) {\n // Update existing card\n const columnIndex = updatedBoard.columns.findIndex((col) =>\n col.cards.some((card) => card.id === editingCard.id)\n )\n if (columnIndex !== -1) {\n const cardIndex = updatedBoard.columns[columnIndex].cards.findIndex(\n (card) => card.id === editingCard.id\n )\n if (cardIndex !== -1) {\n updatedBoard.columns[columnIndex].cards[cardIndex] = {\n ...editingCard,\n ...cardData,\n } as KanbanCard\n }\n }\n } else {\n // Create new card\n const columnIndex = updatedBoard.columns.findIndex(\n (col) => col.id === activeColumnId\n )\n if (columnIndex !== -1) {\n const newCard: KanbanCard = {\n id: `card-${Date.now()}`,\n title: cardData.title!,\n description: cardData.description,\n labels: cardData.labels,\n priority: cardData.priority,\n assignee: cardData.assignee,\n dueDate: cardData.dueDate,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n }\n updatedBoard.columns[columnIndex].cards.push(newCard)\n }\n }\n\n onUpdateBoard(updatedBoard)\n }\n\n const handleDeleteCard = (cardId: string) => {\n const updatedBoard = { ...board }\n const columnIndex = updatedBoard.columns.findIndex((col) =>\n col.cards.some((card) => card.id === cardId)\n )\n if (columnIndex !== -1) {\n updatedBoard.columns[columnIndex].cards = updatedBoard.columns[\n columnIndex\n ].cards.filter((card) => card.id !== cardId)\n onUpdateBoard(updatedBoard)\n }\n }\n\n return (\n <div className={cn(\"flex flex-col h-full\", className)}>\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b\">\n <h1 className=\"text-2xl font-bold\">{board.title}</h1>\n <div className=\"flex items-center gap-3\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search cards...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"pl-9 w-64\"\n />\n </div>\n </div>\n </div>\n\n {/* Board */}\n <div className=\"flex-1 overflow-x-auto\">\n <div className=\"h-full p-4\">\n <DndContext\n sensors={sensors}\n collisionDetection={closestCorners}\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragEnd={handleDragEnd}\n >\n <div className=\"flex gap-6 h-full pb-4\">\n {filteredBoard.columns.map((column) => (\n <KanbanColumnComponent\n key={column.id}\n column={column}\n onAddCard={handleAddCard}\n onEditCard={handleEditCard}\n onDeleteCard={handleDeleteCard}\n />\n ))}\n </div>\n\n <DragOverlay>\n {activeCard ? (\n <KanbanCardComponent\n card={activeCard}\n onEdit={() => {}}\n onDelete={() => {}}\n />\n ) : null}\n </DragOverlay>\n </DndContext>\n </div>\n </div>\n\n {/* Card Form Dialog */}\n <CardFormDialog\n open={cardFormOpen}\n onOpenChange={setCardFormOpen}\n card={editingCard}\n onSave={handleSaveCard}\n labels={board.labels}\n />\n </div>\n )\n}\n\n// Export default for registry\nexport default KanbanBoard\n"
|
|
}
|
|
],
|
|
"type": "components:ui"
|
|
} |